Skip to main content

tenferro_fft/
lib.rs

1//! FFT extension operations for tenferro.
2//!
3//! This crate is an out-of-tree `ExtensionOp` package. The initial
4//! implementation executes on host tensors through `rustfft`; it does not add
5//! FFT to the core `tenferro` backend trait surface. Concrete non-AD execution
6//! uses [`TensorFftExt`] and [`TensorReadFftExt`]. Traced graph construction
7//! uses [`TracedTensorFftExt`].
8//!
9//! # Examples
10//!
11//! ```
12//! use num_complex::Complex64;
13//! use tenferro_cpu::CpuBackend;
14//! use tenferro_runtime::{GraphCompiler, GraphExecutor, TracedTensor};
15//! use tenferro_fft::{FftNorm, TracedTensorFftExt};
16//!
17//! let x = TracedTensor::from_vec_col_major(
18//!     vec![4],
19//!     vec![
20//!         Complex64::new(1.0, 0.0),
21//!         Complex64::new(2.0, 0.0),
22//!         Complex64::new(3.0, 0.0),
23//!         Complex64::new(4.0, 0.0),
24//!     ],
25//! )
26//! .unwrap();
27//! let y = x.fft(None, -1, FftNorm::Backward).unwrap();
28//!
29//! let mut compiler = GraphCompiler::new();
30//! let program = compiler.compile(&y).unwrap();
31//! let mut executor = GraphExecutor::new(CpuBackend::new());
32//! executor.register_extension(tenferro_fft::register_runtime).unwrap();
33//! let out = executor.run(&program).unwrap();
34//! assert_eq!(out.shape(), &[4]);
35//! assert_eq!(out.as_slice::<Complex64>().unwrap()[0], Complex64::new(10.0, 0.0));
36//! ```
37//!
38//! ```
39//! use num_complex::Complex64;
40//! use tenferro_cpu::CpuBackend;
41//! use tenferro_fft::{FftNorm, TensorFftExt};
42//! use tenferro_tensor::Tensor;
43//!
44//! let x = Tensor::from_vec_col_major(vec![4], vec![1.0_f64, 2.0, 3.0, 4.0]).unwrap();
45//! let mut backend = CpuBackend::new();
46//! let out = x.fft(None, -1, FftNorm::Backward, &mut backend).unwrap();
47//!
48//! assert_eq!(out.as_slice::<Complex64>().unwrap()[0], Complex64::new(10.0, 0.0));
49//! ```
50
51use std::any::Any;
52use std::collections::HashMap;
53use std::hash::Hasher;
54use std::mem::MaybeUninit;
55use std::sync::{Arc, Mutex, OnceLock};
56
57#[cfg(feature = "autodiff")]
58use computegraph::types::{LocalValueId, OperationRole, ValueKey, ValueRef};
59use num_complex::Complex;
60use num_traits::{Float, FromPrimitive, Zero};
61use rustfft::{Fft, FftNum, FftPlanner};
62#[cfg(feature = "autodiff")]
63use tenferro_ad::extension::{
64    ExtensionLinearTransposeRule, ExtensionLinearizeRule, ExtensionPrimalVjpRule,
65    ExtensionRegistryError, ExtensionRuleSet,
66};
67use tenferro_extension_macros::define_extension_runtime;
68#[cfg(feature = "autodiff")]
69use tenferro_ops::ad::{transpose_input::TransposeInputRef, PrimitiveRuleBuilder};
70#[cfg(feature = "autodiff")]
71use tenferro_ops::std_tensor_op::StdTensorOp;
72#[cfg(feature = "autodiff")]
73use tenferro_ops::ShapeGuardContext;
74use tenferro_ops::SymDim;
75use tenferro_runtime::extension::{apply, ExtensionExecutionContext, ExtensionOp, HostReference};
76use tenferro_runtime::{Error, Result, TracedTensor};
77use tenferro_tensor::{
78    DType, DeviceKind, MemoryKind, Placement, Tensor, TensorBackend, TensorRead, TypedTensor,
79};
80#[cfg(feature = "autodiff")]
81use tidu::{ADRuleError, ADRuleKind, ADRuleResult};
82
83/// Extension family id used by the tenferro FFT extension.
84///
85/// # Examples
86///
87/// ```
88/// assert_eq!(
89///     tenferro_fft::FFT_EXTENSION_FAMILY_ID,
90///     "tenferro-fft.fft.v1"
91/// );
92/// ```
93pub const FFT_EXTENSION_FAMILY_ID: &str = "tenferro-fft.fft.v1";
94
95/// FFT extension methods for [`TracedTensor`].
96pub trait TracedTensorFftExt {
97    fn fft(&self, n: Option<usize>, axis: isize, norm: FftNorm) -> Result<TracedTensor>;
98    fn ifft(&self, n: Option<usize>, axis: isize, norm: FftNorm) -> Result<TracedTensor>;
99    fn rfft(&self, n: Option<usize>, axis: isize, norm: FftNorm) -> Result<TracedTensor>;
100    fn irfft(&self, n: Option<usize>, axis: isize, norm: FftNorm) -> Result<TracedTensor>;
101}
102
103impl TracedTensorFftExt for TracedTensor {
104    fn fft(&self, n: Option<usize>, axis: isize, norm: FftNorm) -> Result<TracedTensor> {
105        fft(self, n, axis, norm)
106    }
107
108    fn ifft(&self, n: Option<usize>, axis: isize, norm: FftNorm) -> Result<TracedTensor> {
109        ifft(self, n, axis, norm)
110    }
111
112    fn rfft(&self, n: Option<usize>, axis: isize, norm: FftNorm) -> Result<TracedTensor> {
113        rfft(self, n, axis, norm)
114    }
115
116    fn irfft(&self, n: Option<usize>, axis: isize, norm: FftNorm) -> Result<TracedTensor> {
117        irfft(self, n, axis, norm)
118    }
119}
120
121/// Backend-explicit FFT methods for concrete [`Tensor`] values.
122///
123/// This is the non-AD immediate execution surface. It uses unsuffixed method
124/// names because the receiver is an owned compact tensor value. Use
125/// [`TensorReadFftExt`] when the input is a borrowed view or other
126/// [`TensorRead`] value.
127///
128/// # Examples
129///
130/// ```
131/// use num_complex::Complex64;
132/// use tenferro_cpu::CpuBackend;
133/// use tenferro_fft::{FftNorm, TensorFftExt};
134/// use tenferro_tensor::Tensor;
135///
136/// let input = Tensor::from_vec_col_major(vec![4], vec![1.0_f64, 2.0, 3.0, 4.0])?;
137/// let mut backend = CpuBackend::new();
138///
139/// let spectrum = input.fft(None, -1, FftNorm::Backward, &mut backend)?;
140/// assert_eq!(spectrum.shape(), &[4]);
141/// assert_eq!(spectrum.as_slice::<Complex64>()?[0], Complex64::new(10.0, 0.0));
142/// # Ok::<(), tenferro_tensor::Error>(())
143/// ```
144pub trait TensorFftExt {
145    /// Execute a one-dimensional FFT along `axis`.
146    fn fft<B: TensorBackend>(
147        &self,
148        n: Option<usize>,
149        axis: isize,
150        norm: FftNorm,
151        backend: &mut B,
152    ) -> tenferro_tensor::Result<Tensor>;
153
154    /// Execute a one-dimensional inverse FFT along `axis`.
155    fn ifft<B: TensorBackend>(
156        &self,
157        n: Option<usize>,
158        axis: isize,
159        norm: FftNorm,
160        backend: &mut B,
161    ) -> tenferro_tensor::Result<Tensor>;
162
163    /// Execute a one-dimensional real FFT along `axis`.
164    fn rfft<B: TensorBackend>(
165        &self,
166        n: Option<usize>,
167        axis: isize,
168        norm: FftNorm,
169        backend: &mut B,
170    ) -> tenferro_tensor::Result<Tensor>;
171
172    /// Execute a one-dimensional inverse real FFT along `axis`.
173    fn irfft<B: TensorBackend>(
174        &self,
175        n: Option<usize>,
176        axis: isize,
177        norm: FftNorm,
178        backend: &mut B,
179    ) -> tenferro_tensor::Result<Tensor>;
180}
181
182impl TensorFftExt for Tensor {
183    fn fft<B: TensorBackend>(
184        &self,
185        n: Option<usize>,
186        axis: isize,
187        norm: FftNorm,
188        backend: &mut B,
189    ) -> tenferro_tensor::Result<Tensor> {
190        let op = concrete_fft_op(
191            "TensorFftExt::fft",
192            concrete_fft_kind("TensorFftExt::fft", self.dtype())?,
193            self.shape(),
194            n,
195            axis,
196            norm,
197        )?;
198        execute_concrete_fft_op(self, &op, backend)
199    }
200
201    fn ifft<B: TensorBackend>(
202        &self,
203        n: Option<usize>,
204        axis: isize,
205        norm: FftNorm,
206        backend: &mut B,
207    ) -> tenferro_tensor::Result<Tensor> {
208        let op = concrete_fft_op(
209            "TensorFftExt::ifft",
210            concrete_ifft_kind("TensorFftExt::ifft", self.dtype())?,
211            self.shape(),
212            n,
213            axis,
214            norm,
215        )?;
216        execute_concrete_fft_op(self, &op, backend)
217    }
218
219    fn rfft<B: TensorBackend>(
220        &self,
221        n: Option<usize>,
222        axis: isize,
223        norm: FftNorm,
224        backend: &mut B,
225    ) -> tenferro_tensor::Result<Tensor> {
226        let op = concrete_fft_op(
227            "TensorFftExt::rfft",
228            concrete_rfft_kind("TensorFftExt::rfft", self.dtype())?,
229            self.shape(),
230            n,
231            axis,
232            norm,
233        )?;
234        execute_concrete_fft_op(self, &op, backend)
235    }
236
237    fn irfft<B: TensorBackend>(
238        &self,
239        n: Option<usize>,
240        axis: isize,
241        norm: FftNorm,
242        backend: &mut B,
243    ) -> tenferro_tensor::Result<Tensor> {
244        let op = concrete_fft_op(
245            "TensorFftExt::irfft",
246            concrete_irfft_kind("TensorFftExt::irfft", self.dtype())?,
247            self.shape(),
248            n,
249            axis,
250            norm,
251        )?;
252        execute_concrete_fft_op(self, &op, backend)
253    }
254}
255
256/// Backend-explicit FFT methods for read-only tensor inputs.
257///
258/// The `_read` suffix follows the repository convention for APIs that
259/// explicitly accept [`TensorRead`] values such as borrowed views.
260///
261/// # Examples
262///
263/// ```
264/// use num_complex::Complex64;
265/// use tenferro_cpu::CpuBackend;
266/// use tenferro_fft::{FftNorm, TensorReadFftExt};
267/// use tenferro_tensor::{TensorRead, TensorView};
268///
269/// let shape = [4usize];
270/// let data = [1.0_f64, 2.0, 3.0, 4.0];
271/// let input = TensorRead::from_view(TensorView::f64(&shape, &data)?);
272/// let mut backend = CpuBackend::new();
273///
274/// let spectrum = input.fft_read(None, -1, FftNorm::Backward, &mut backend)?;
275/// assert_eq!(spectrum.as_slice::<Complex64>()?[0], Complex64::new(10.0, 0.0));
276/// # Ok::<(), tenferro_tensor::Error>(())
277/// ```
278pub trait TensorReadFftExt {
279    /// Execute a one-dimensional FFT along `axis`.
280    fn fft_read<B: TensorBackend>(
281        &self,
282        n: Option<usize>,
283        axis: isize,
284        norm: FftNorm,
285        backend: &mut B,
286    ) -> tenferro_tensor::Result<Tensor>;
287
288    /// Execute a one-dimensional inverse FFT along `axis`.
289    fn ifft_read<B: TensorBackend>(
290        &self,
291        n: Option<usize>,
292        axis: isize,
293        norm: FftNorm,
294        backend: &mut B,
295    ) -> tenferro_tensor::Result<Tensor>;
296
297    /// Execute a one-dimensional real FFT along `axis`.
298    fn rfft_read<B: TensorBackend>(
299        &self,
300        n: Option<usize>,
301        axis: isize,
302        norm: FftNorm,
303        backend: &mut B,
304    ) -> tenferro_tensor::Result<Tensor>;
305
306    /// Execute a one-dimensional inverse real FFT along `axis`.
307    fn irfft_read<B: TensorBackend>(
308        &self,
309        n: Option<usize>,
310        axis: isize,
311        norm: FftNorm,
312        backend: &mut B,
313    ) -> tenferro_tensor::Result<Tensor>;
314}
315
316impl TensorReadFftExt for TensorRead<'_> {
317    fn fft_read<B: TensorBackend>(
318        &self,
319        n: Option<usize>,
320        axis: isize,
321        norm: FftNorm,
322        backend: &mut B,
323    ) -> tenferro_tensor::Result<Tensor> {
324        execute_concrete_fft_read_op(
325            self,
326            concrete_fft_kind("TensorReadFftExt::fft_read", self.dtype())?,
327            "TensorReadFftExt::fft_read",
328            n,
329            axis,
330            norm,
331            backend,
332        )
333    }
334
335    fn ifft_read<B: TensorBackend>(
336        &self,
337        n: Option<usize>,
338        axis: isize,
339        norm: FftNorm,
340        backend: &mut B,
341    ) -> tenferro_tensor::Result<Tensor> {
342        execute_concrete_fft_read_op(
343            self,
344            concrete_ifft_kind("TensorReadFftExt::ifft_read", self.dtype())?,
345            "TensorReadFftExt::ifft_read",
346            n,
347            axis,
348            norm,
349            backend,
350        )
351    }
352
353    fn rfft_read<B: TensorBackend>(
354        &self,
355        n: Option<usize>,
356        axis: isize,
357        norm: FftNorm,
358        backend: &mut B,
359    ) -> tenferro_tensor::Result<Tensor> {
360        execute_concrete_fft_read_op(
361            self,
362            concrete_rfft_kind("TensorReadFftExt::rfft_read", self.dtype())?,
363            "TensorReadFftExt::rfft_read",
364            n,
365            axis,
366            norm,
367            backend,
368        )
369    }
370
371    fn irfft_read<B: TensorBackend>(
372        &self,
373        n: Option<usize>,
374        axis: isize,
375        norm: FftNorm,
376        backend: &mut B,
377    ) -> tenferro_tensor::Result<Tensor> {
378        execute_concrete_fft_read_op(
379            self,
380            concrete_irfft_kind("TensorReadFftExt::irfft_read", self.dtype())?,
381            "TensorReadFftExt::irfft_read",
382            n,
383            axis,
384            norm,
385            backend,
386        )
387    }
388}
389
390/// FFT normalization convention.
391///
392/// `Backward` matches NumPy, JAX, and PyTorch defaults: the forward transform
393/// is unscaled and the inverse transform is scaled by `1 / n`.
394///
395/// # Examples
396///
397/// ```
398/// use tenferro_fft::FftNorm;
399///
400/// assert_eq!(FftNorm::default(), FftNorm::Backward);
401/// ```
402#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
403pub enum FftNorm {
404    /// Scale inverse transforms by `1 / n`.
405    #[default]
406    Backward,
407    /// Scale forward transforms by `1 / n`.
408    Forward,
409    /// Scale both forward and inverse transforms by `1 / sqrt(n)`.
410    Ortho,
411}
412
413#[cfg(feature = "autodiff")]
414impl FftNorm {
415    fn c2c_adjoint(self) -> Self {
416        match self {
417            Self::Backward => Self::Forward,
418            Self::Forward => Self::Backward,
419            Self::Ortho => Self::Ortho,
420        }
421    }
422}
423
424#[derive(Clone, Copy, Debug, Eq, PartialEq)]
425enum FftKind {
426    C2C { forward: bool },
427    R2C { onesided: bool },
428    C2R,
429}
430
431#[derive(Clone, Debug, PartialEq)]
432struct FftOp {
433    kind: FftKind,
434    axis: usize,
435    n: Option<usize>,
436    norm: FftNorm,
437}
438
439impl FftOp {
440    fn new(kind: FftKind, axis: usize, n: Option<usize>, norm: FftNorm) -> Self {
441        Self {
442            kind,
443            axis,
444            n,
445            norm,
446        }
447    }
448
449    #[cfg(feature = "autodiff")]
450    fn c2c_adjoint(&self) -> Option<Self> {
451        match self.kind {
452            FftKind::C2C { forward } => Some(Self {
453                kind: FftKind::C2C { forward: !forward },
454                axis: self.axis,
455                n: self.n,
456                norm: self.norm.c2c_adjoint(),
457            }),
458            FftKind::R2C { .. } | FftKind::C2R => None,
459        }
460    }
461}
462
463impl ExtensionOp for FftOp {
464    fn family_id(&self) -> &'static str {
465        FFT_EXTENSION_FAMILY_ID
466    }
467
468    fn payload_hash(&self, hasher: &mut dyn Hasher) {
469        let kind = match self.kind {
470            FftKind::C2C { forward: true } => 0,
471            FftKind::C2C { forward: false } => 1,
472            FftKind::R2C { onesided: true } => 2,
473            FftKind::R2C { onesided: false } => 3,
474            FftKind::C2R => 4,
475        };
476        hasher.write_u8(kind);
477        hasher.write_usize(self.axis);
478        match self.n {
479            Some(n) => {
480                hasher.write_u8(1);
481                hasher.write_usize(n);
482            }
483            None => hasher.write_u8(0),
484        }
485        let norm = match self.norm {
486            FftNorm::Backward => 0,
487            FftNorm::Forward => 1,
488            FftNorm::Ortho => 2,
489        };
490        hasher.write_u8(norm);
491    }
492
493    fn payload_eq(&self, other: &dyn ExtensionOp) -> bool {
494        other
495            .as_any()
496            .downcast_ref::<FftOp>()
497            .is_some_and(|that| self == that)
498    }
499
500    fn clone_arc(&self) -> Arc<dyn ExtensionOp> {
501        Arc::new(self.clone())
502    }
503
504    fn as_any(&self) -> &dyn Any {
505        self
506    }
507
508    fn input_count(&self) -> usize {
509        1
510    }
511
512    fn output_count(&self) -> usize {
513        1
514    }
515
516    fn infer_output_meta(
517        &self,
518        input_dtypes: &[DType],
519        input_shapes: &[&[SymDim]],
520    ) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
521        let [input_dtype] = input_dtypes else {
522            return Err(tenferro_tensor::Error::InvalidConfig {
523                op: "tenferro-fft",
524                message: format!("expected 1 input dtype, got {}", input_dtypes.len()),
525            });
526        };
527        let [input_shape] = input_shapes else {
528            return Err(tenferro_tensor::Error::InvalidConfig {
529                op: "tenferro-fft",
530                message: format!("expected 1 input shape, got {}", input_shapes.len()),
531            });
532        };
533        if self.axis >= input_shape.len() {
534            return Err(tenferro_tensor::Error::AxisOutOfBounds {
535                op: "tenferro-fft",
536                axis: self.axis,
537                rank: input_shape.len(),
538            });
539        }
540
541        let mut out_shape = input_shape.to_vec();
542        let output_dtype = match self.kind {
543            FftKind::C2C { .. } => {
544                if !matches!(input_dtype, DType::C32 | DType::C64) {
545                    return Err(tenferro_tensor::Error::backend_failure(
546                        "tenferro-fft",
547                        format!("unsupported dtype {input_dtype:?} for complex FFT"),
548                    ));
549                }
550                *input_dtype
551            }
552            FftKind::R2C { onesided } => {
553                let len = transform_len_dim(self.n, &input_shape[self.axis]);
554                out_shape[self.axis] = if onesided { len / 2usize + 1usize } else { len };
555                match input_dtype {
556                    DType::F32 => DType::C32,
557                    DType::F64 => DType::C64,
558                    _ => {
559                        return Err(tenferro_tensor::Error::backend_failure(
560                            "tenferro-fft",
561                            format!("unsupported dtype {input_dtype:?} for real FFT"),
562                        ));
563                    }
564                }
565            }
566            FftKind::C2R => {
567                out_shape[self.axis] = output_dim_c2r(&input_shape[self.axis], self.n)?;
568                match input_dtype {
569                    DType::C32 => DType::F32,
570                    DType::C64 => DType::F64,
571                    _ => {
572                        return Err(tenferro_tensor::Error::backend_failure(
573                            "tenferro-fft",
574                            format!("unsupported dtype {input_dtype:?} for inverse real FFT"),
575                        ));
576                    }
577                }
578            }
579        };
580
581        if matches!(self.kind, FftKind::C2C { .. }) {
582            out_shape[self.axis] = transform_len_dim(self.n, &input_shape[self.axis]);
583        }
584
585        Ok(vec![(output_dtype, out_shape)])
586    }
587
588    fn host_reference(&self) -> Option<&dyn HostReference> {
589        Some(self)
590    }
591}
592
593impl HostReference for FftOp {
594    fn execute(&self, inputs: &[&Tensor]) -> tenferro_tensor::Result<Vec<Tensor>> {
595        execute_host_fft_op(self, inputs)
596    }
597}
598
599fn execute_host_fft_op(op: &FftOp, inputs: &[&Tensor]) -> tenferro_tensor::Result<Vec<Tensor>> {
600    if inputs.len() != 1 {
601        return Err(tenferro_tensor::Error::InvalidConfig {
602            op: "tenferro-fft",
603            message: format!("expected 1 input, got {}", inputs.len()),
604        });
605    }
606    validate_host_fft_input(fft_op_name(op.kind), inputs[0])?;
607
608    let output = match (op.kind, inputs[0]) {
609        (FftKind::C2C { forward }, Tensor::C64(input)) => {
610            Tensor::C64(TypedTensor::from_vec_col_major(
611                output_shape_c2c(input.shape(), op.axis, op.n)?,
612                execute_c2c(input, op.axis, op.n, forward, op.norm)?,
613            )?)
614        }
615        (FftKind::C2C { forward }, Tensor::C32(input)) => {
616            Tensor::C32(TypedTensor::from_vec_col_major(
617                output_shape_c2c(input.shape(), op.axis, op.n)?,
618                execute_c2c(input, op.axis, op.n, forward, op.norm)?,
619            )?)
620        }
621        (FftKind::R2C { onesided }, Tensor::F64(input)) => {
622            Tensor::C64(TypedTensor::from_vec_col_major(
623                output_shape_r2c(input.shape(), op.axis, op.n, onesided)?,
624                execute_r2c(input, op.axis, op.n, onesided, op.norm)?,
625            )?)
626        }
627        (FftKind::R2C { onesided }, Tensor::F32(input)) => {
628            Tensor::C32(TypedTensor::from_vec_col_major(
629                output_shape_r2c(input.shape(), op.axis, op.n, onesided)?,
630                execute_r2c(input, op.axis, op.n, onesided, op.norm)?,
631            )?)
632        }
633        (FftKind::C2R, Tensor::C64(input)) => Tensor::F64(TypedTensor::from_vec_col_major(
634            output_shape_c2r(input.shape(), op.axis, op.n)?,
635            execute_c2r(input, op.axis, op.n, op.norm)?,
636        )?),
637        (FftKind::C2R, Tensor::C32(input)) => Tensor::F32(TypedTensor::from_vec_col_major(
638            output_shape_c2r(input.shape(), op.axis, op.n)?,
639            execute_c2r(input, op.axis, op.n, op.norm)?,
640        )?),
641        (kind, other) => {
642            return Err(tenferro_tensor::Error::DTypeMismatch {
643                op: match kind {
644                    FftKind::C2C { .. } => "fft",
645                    FftKind::R2C { .. } => "rfft",
646                    FftKind::C2R => "irfft",
647                },
648                lhs: expected_dtype_for(kind),
649                rhs: other.dtype(),
650            });
651        }
652    };
653    Ok(vec![output])
654}
655
656fn execute_concrete_fft_op<B: TensorBackend>(
657    input: &Tensor,
658    op: &FftOp,
659    backend: &mut B,
660) -> tenferro_tensor::Result<Tensor> {
661    backend.with_backend_session(|_exec| single_fft_output(execute_host_fft_op(op, &[input])?))
662}
663
664#[allow(clippy::too_many_arguments)]
665fn execute_concrete_fft_read_op<B: TensorBackend>(
666    input: &TensorRead<'_>,
667    kind: FftKind,
668    op_name: &'static str,
669    n: Option<usize>,
670    axis: isize,
671    norm: FftNorm,
672    backend: &mut B,
673) -> tenferro_tensor::Result<Tensor> {
674    let op = concrete_fft_op(op_name, kind, input.shape(), n, axis, norm)?;
675    let materialized = input.to_tensor()?;
676    execute_concrete_fft_op(&materialized, &op, backend)
677}
678
679fn single_fft_output(mut outputs: Vec<Tensor>) -> tenferro_tensor::Result<Tensor> {
680    if outputs.len() != 1 {
681        return Err(tenferro_tensor::Error::InvalidConfig {
682            op: "tenferro-fft",
683            message: format!("expected 1 FFT output, got {}", outputs.len()),
684        });
685    }
686    Ok(outputs.remove(0))
687}
688
689fn concrete_fft_op(
690    op: &'static str,
691    kind: FftKind,
692    input_shape: &[usize],
693    n: Option<usize>,
694    axis: isize,
695    norm: FftNorm,
696) -> tenferro_tensor::Result<FftOp> {
697    validate_concrete_n(op, n)?;
698    let axis = normalize_concrete_axis(op, axis, input_shape.len())?;
699    validate_concrete_transform_len(op, input_shape, n, axis)?;
700    if matches!(kind, FftKind::C2R) {
701        output_shape_c2r(input_shape, axis, n)?;
702    }
703    Ok(FftOp::new(kind, axis, n, norm))
704}
705
706fn concrete_fft_kind(op: &'static str, dtype: DType) -> tenferro_tensor::Result<FftKind> {
707    match dtype {
708        DType::C32 | DType::C64 => Ok(FftKind::C2C { forward: true }),
709        DType::F32 | DType::F64 => Ok(FftKind::R2C { onesided: false }),
710        DType::I32 | DType::I64 | DType::Bool => Err(tensor_fft_config_error(
711            op,
712            format!("fft expects real or complex floating input, got {dtype:?}"),
713        )),
714    }
715}
716
717fn concrete_ifft_kind(op: &'static str, dtype: DType) -> tenferro_tensor::Result<FftKind> {
718    match dtype {
719        DType::C32 | DType::C64 => Ok(FftKind::C2C { forward: false }),
720        DType::F32 | DType::F64 | DType::I32 | DType::I64 | DType::Bool => Err(
721            tensor_fft_config_error(op, format!("ifft expects C32 or C64 input; got {dtype:?}")),
722        ),
723    }
724}
725
726fn concrete_rfft_kind(op: &'static str, dtype: DType) -> tenferro_tensor::Result<FftKind> {
727    match dtype {
728        DType::F32 | DType::F64 => Ok(FftKind::R2C { onesided: true }),
729        DType::C32 | DType::C64 | DType::I32 | DType::I64 | DType::Bool => Err(
730            tensor_fft_config_error(op, format!("rfft expects F32 or F64 input; got {dtype:?}")),
731        ),
732    }
733}
734
735fn concrete_irfft_kind(op: &'static str, dtype: DType) -> tenferro_tensor::Result<FftKind> {
736    match dtype {
737        DType::C32 | DType::C64 => Ok(FftKind::C2R),
738        DType::F32 | DType::F64 | DType::I32 | DType::I64 | DType::Bool => Err(
739            tensor_fft_config_error(op, format!("irfft expects C32 or C64 input; got {dtype:?}")),
740        ),
741    }
742}
743
744fn validate_concrete_n(op: &'static str, n: Option<usize>) -> tenferro_tensor::Result<()> {
745    if n == Some(0) {
746        return Err(tensor_fft_config_error(
747            op,
748            "tenferro-fft transform length n must be positive",
749        ));
750    }
751    Ok(())
752}
753
754fn validate_concrete_transform_len(
755    op: &'static str,
756    input_shape: &[usize],
757    n: Option<usize>,
758    axis: usize,
759) -> tenferro_tensor::Result<()> {
760    if n.is_none() && input_shape.get(axis).copied() == Some(0) {
761        return Err(tensor_fft_config_error(
762            op,
763            "tenferro-fft transform length n must be positive",
764        ));
765    }
766    Ok(())
767}
768
769fn normalize_concrete_axis(
770    op: &'static str,
771    axis: isize,
772    rank: usize,
773) -> tenferro_tensor::Result<usize> {
774    if rank == 0 {
775        return Err(tensor_fft_config_error(
776            op,
777            "tenferro-fft requires rank >= 1",
778        ));
779    }
780    let normalized = if axis >= 0 {
781        axis as usize
782    } else {
783        rank.checked_sub(axis.unsigned_abs()).ok_or_else(|| {
784            tensor_fft_config_error(
785                op,
786                format!("tenferro-fft axis {axis} out of bounds for rank {rank}"),
787            )
788        })?
789    };
790    if normalized >= rank {
791        return Err(tensor_fft_config_error(
792            op,
793            format!("tenferro-fft axis {axis} out of bounds for rank {rank}"),
794        ));
795    }
796    Ok(normalized)
797}
798
799fn tensor_fft_config_error(
800    op: &'static str,
801    message: impl std::fmt::Display,
802) -> tenferro_tensor::Error {
803    tenferro_tensor::Error::InvalidConfig {
804        op,
805        message: message.to_string(),
806    }
807}
808
809fn tensor_placement(input: &Tensor) -> &Placement {
810    input.placement()
811}
812
813fn tensor_has_backend_buffer(input: &Tensor) -> bool {
814    input.is_backend_buffer()
815}
816
817fn validate_host_fft_input(op: &'static str, input: &Tensor) -> tenferro_tensor::Result<()> {
818    let placement = tensor_placement(input);
819    let is_device = matches!(placement.memory_kind, MemoryKind::Device);
820    if !is_device && !tensor_has_backend_buffer(input) {
821        return Ok(());
822    }
823
824    let location = match placement.device.as_ref().map(|device| &device.kind) {
825        Some(DeviceKind::Gpu(kind)) => format!("GPU backend {kind:?}"),
826        Some(kind) => format!("device kind {kind:?}"),
827        None if is_device => "device tensor without device metadata".to_string(),
828        None => "backend buffer".to_string(),
829    };
830    Err(tenferro_tensor::Error::backend_failure(
831        op,
832        format!(
833            "tenferro-fft supports host tensors only; unsupported {location} input; \
834             download the tensor to CPU before FFT"
835        ),
836    ))
837}
838
839#[cfg(feature = "autodiff")]
840#[derive(Debug)]
841struct FftAdRule;
842
843#[cfg(feature = "autodiff")]
844impl ExtensionLinearizeRule for FftAdRule {
845    fn family_id(&self) -> &'static str {
846        FFT_EXTENSION_FAMILY_ID
847    }
848
849    fn linearize(
850        &self,
851        op: &dyn ExtensionOp,
852        builder: &mut dyn PrimitiveRuleBuilder,
853        _primal_in: &[ValueKey<StdTensorOp>],
854        _primal_out: &[ValueKey<StdTensorOp>],
855        tangent_in: &[Option<LocalValueId>],
856        _ctx: &mut ShapeGuardContext,
857    ) -> ADRuleResult<Vec<Option<LocalValueId>>> {
858        let fft_op = fft_payload(op, ADRuleKind::Jvp)?;
859        if !matches!(fft_op.kind, FftKind::C2C { .. }) {
860            return Err(ADRuleError::unsupported(
861                fft_ad_family_id(fft_op.kind),
862                ADRuleKind::Jvp,
863            ));
864        }
865
866        match tangent_in[0] {
867            Some(dx) => {
868                let outputs = builder.add_operation(
869                    StdTensorOp::Extension(Arc::new(fft_op.clone())),
870                    vec![ValueRef::Local(dx)],
871                    OperationRole::Linearized {
872                        active_mask: vec![true],
873                    },
874                );
875                Ok(vec![Some(outputs[0])])
876            }
877            None => Ok(vec![None]),
878        }
879    }
880}
881
882#[cfg(feature = "autodiff")]
883impl ExtensionLinearTransposeRule for FftAdRule {
884    fn family_id(&self) -> &'static str {
885        FFT_EXTENSION_FAMILY_ID
886    }
887
888    fn linear_transpose(
889        &self,
890        op: &dyn ExtensionOp,
891        builder: &mut dyn PrimitiveRuleBuilder,
892        cotangent_out: &[Option<LocalValueId>],
893        inputs: &[tidu::PrimitiveTransposeInput<StdTensorOp>],
894        active_mask: &[bool],
895        ctx: &mut ShapeGuardContext,
896    ) -> ADRuleResult<Vec<Option<LocalValueId>>> {
897        let inputs: Vec<_> = inputs.iter().map(TransposeInputRef::new).collect();
898        transpose_fft_adjoint_from_transpose_inputs(
899            op,
900            builder,
901            cotangent_out,
902            &inputs,
903            active_mask,
904            ctx,
905        )
906    }
907}
908
909#[cfg(feature = "autodiff")]
910impl ExtensionPrimalVjpRule for FftAdRule {
911    fn family_id(&self) -> &'static str {
912        FFT_EXTENSION_FAMILY_ID
913    }
914
915    fn primal_vjp(
916        &self,
917        op: &dyn ExtensionOp,
918        builder: &mut dyn PrimitiveRuleBuilder,
919        cotangent_out: &[Option<LocalValueId>],
920        inputs: &[ValueRef<StdTensorOp>],
921        ctx: &mut ShapeGuardContext,
922    ) -> ADRuleResult<Vec<Option<LocalValueId>>> {
923        transpose_fft_adjoint(op, builder, cotangent_out, inputs, None, ctx)
924    }
925}
926
927#[cfg(feature = "autodiff")]
928fn transpose_fft_adjoint(
929    op: &dyn ExtensionOp,
930    builder: &mut dyn PrimitiveRuleBuilder,
931    cotangent_out: &[Option<LocalValueId>],
932    inputs: &[ValueRef<StdTensorOp>],
933    active_mask: Option<&[bool]>,
934    ctx: &mut ShapeGuardContext,
935) -> ADRuleResult<Vec<Option<LocalValueId>>> {
936    let Some((adjoint, fft_op)) = emit_c2c_adjoint(op, builder, cotangent_out, active_mask)? else {
937        return Ok(vec![None]);
938    };
939    let restored = restore_c2c_adjoint_input_length(builder, adjoint, inputs, fft_op, ctx)?;
940    Ok(vec![Some(restored)])
941}
942
943#[cfg(feature = "autodiff")]
944fn transpose_fft_adjoint_from_transpose_inputs(
945    op: &dyn ExtensionOp,
946    builder: &mut dyn PrimitiveRuleBuilder,
947    cotangent_out: &[Option<LocalValueId>],
948    inputs: &[TransposeInputRef<'_>],
949    active_mask: &[bool],
950    ctx: &mut ShapeGuardContext,
951) -> ADRuleResult<Vec<Option<LocalValueId>>> {
952    let Some((adjoint, fft_op)) = emit_c2c_adjoint(op, builder, cotangent_out, Some(active_mask))?
953    else {
954        return Ok(vec![None]);
955    };
956    let restored = restore_c2c_adjoint_input_length_from_transpose_input(
957        builder, adjoint, inputs, fft_op, ctx,
958    )?;
959    Ok(vec![Some(restored)])
960}
961
962#[cfg(feature = "autodiff")]
963fn emit_c2c_adjoint<'a>(
964    op: &'a dyn ExtensionOp,
965    builder: &mut dyn PrimitiveRuleBuilder,
966    cotangent_out: &[Option<LocalValueId>],
967    active_mask: Option<&[bool]>,
968) -> ADRuleResult<Option<(LocalValueId, &'a FftOp)>> {
969    let fft_op = fft_payload(op, ADRuleKind::Transpose)?;
970    if !matches!(fft_op.kind, FftKind::C2C { .. }) {
971        return Err(ADRuleError::unsupported(
972            fft_ad_family_id(fft_op.kind),
973            ADRuleKind::Transpose,
974        ));
975    }
976    if active_mask.is_some_and(|mask| !mask.first().copied().unwrap_or(false)) {
977        return Ok(None);
978    }
979
980    let Some(ct) = cotangent_out.first().copied().flatten() else {
981        return Ok(None);
982    };
983    let adjoint_op = fft_op
984        .c2c_adjoint()
985        .ok_or_else(|| ADRuleError::unsupported(FFT_EXTENSION_FAMILY_ID, ADRuleKind::Transpose))?;
986    let outputs = builder.add_operation(
987        StdTensorOp::Extension(Arc::new(adjoint_op)),
988        vec![ValueRef::Local(ct)],
989        OperationRole::Linearized {
990            active_mask: vec![true],
991        },
992    );
993    Ok(Some((outputs[0], fft_op)))
994}
995
996#[cfg(feature = "autodiff")]
997fn restore_c2c_adjoint_input_length(
998    builder: &mut dyn PrimitiveRuleBuilder,
999    adjoint: LocalValueId,
1000    inputs: &[ValueRef<StdTensorOp>],
1001    fft_op: &FftOp,
1002    ctx: &mut ShapeGuardContext,
1003) -> ADRuleResult<LocalValueId> {
1004    let Some(transform_len) = fft_op.n else {
1005        return Ok(adjoint);
1006    };
1007    let Some(input) = inputs.first() else {
1008        return Err(ADRuleError::invalid_input(
1009            FFT_EXTENSION_FAMILY_ID,
1010            ADRuleKind::Transpose,
1011            "FFT transpose rule expected one primal input",
1012        ));
1013    };
1014    if ctx
1015        .shape_of(input)
1016        .ok()
1017        .and_then(|shape| shape.get(fft_op.axis).and_then(SymDim::constant_value))
1018        == Some(transform_len)
1019    {
1020        return Ok(adjoint);
1021    }
1022
1023    let size = builder.add_operation(
1024        StdTensorOp::ShapeOf { axis: fft_op.axis },
1025        vec![input.clone()],
1026        OperationRole::Linearized {
1027            active_mask: vec![false],
1028        },
1029    )[0];
1030    let truncated = builder.add_operation(
1031        StdTensorOp::DynamicTruncate { axis: fft_op.axis },
1032        vec![ValueRef::Local(adjoint), ValueRef::Local(size)],
1033        OperationRole::Linearized {
1034            active_mask: vec![true, false],
1035        },
1036    )[0];
1037    let padded = builder.add_operation(
1038        StdTensorOp::PadToMatch { axis: fft_op.axis },
1039        vec![ValueRef::Local(truncated), input.clone()],
1040        OperationRole::Linearized {
1041            active_mask: vec![true, false],
1042        },
1043    )[0];
1044    Ok(padded)
1045}
1046
1047#[cfg(feature = "autodiff")]
1048fn restore_c2c_adjoint_input_length_from_transpose_input(
1049    builder: &mut dyn PrimitiveRuleBuilder,
1050    adjoint: LocalValueId,
1051    inputs: &[TransposeInputRef<'_>],
1052    fft_op: &FftOp,
1053    ctx: &mut ShapeGuardContext,
1054) -> ADRuleResult<LocalValueId> {
1055    let Some(transform_len) = fft_op.n else {
1056        return Ok(adjoint);
1057    };
1058    let Some(input) = inputs.first() else {
1059        return Err(ADRuleError::invalid_input(
1060            FFT_EXTENSION_FAMILY_ID,
1061            ADRuleKind::Transpose,
1062            "FFT transpose rule expected one primal input",
1063        ));
1064    };
1065    let metadata = input.metadata_value();
1066    if ctx
1067        .shape_of(&metadata)
1068        .ok()
1069        .and_then(|shape| shape.get(fft_op.axis).and_then(SymDim::constant_value))
1070        == Some(transform_len)
1071    {
1072        return Ok(adjoint);
1073    }
1074
1075    let shape_source = input.shape_source_value(FFT_EXTENSION_FAMILY_ID, 0)?;
1076    let size = builder.add_operation(
1077        StdTensorOp::ShapeOf { axis: fft_op.axis },
1078        vec![shape_source.clone()],
1079        OperationRole::Linearized {
1080            active_mask: vec![false],
1081        },
1082    )[0];
1083    let truncated = builder.add_operation(
1084        StdTensorOp::DynamicTruncate { axis: fft_op.axis },
1085        vec![ValueRef::Local(adjoint), ValueRef::Local(size)],
1086        OperationRole::Linearized {
1087            active_mask: vec![true, false],
1088        },
1089    )[0];
1090    let padded = builder.add_operation(
1091        StdTensorOp::PadToMatch { axis: fft_op.axis },
1092        vec![ValueRef::Local(truncated), shape_source],
1093        OperationRole::Linearized {
1094            active_mask: vec![true, false],
1095        },
1096    )[0];
1097    Ok(padded)
1098}
1099
1100/// Return the explicit FFT extension AD rule set.
1101#[cfg(feature = "autodiff")]
1102pub fn ad_rules() -> std::result::Result<ExtensionRuleSet, ExtensionRegistryError> {
1103    ExtensionRuleSet::new()
1104        .with_linearize(Arc::new(FftAdRule))?
1105        .with_linear_transpose(Arc::new(FftAdRule))?
1106        .with_primal_vjp(Arc::new(FftAdRule))
1107}
1108
1109fn execute_fft_extension<B: TensorBackend + 'static>(
1110    op: &FftOp,
1111    inputs: &[&Tensor],
1112    _ctx: &mut ExtensionExecutionContext<'_, B>,
1113) -> tenferro_tensor::Result<Vec<Tensor>> {
1114    execute_host_fft_op(op, inputs)
1115}
1116
1117fn execute_fft_extension_reads<B: TensorBackend + 'static>(
1118    op: &FftOp,
1119    inputs: &[TensorRead<'_>],
1120    ctx: &mut ExtensionExecutionContext<'_, B>,
1121) -> tenferro_tensor::Result<Vec<Tensor>> {
1122    let _ = ctx;
1123    // rustfft consumes compact host tensors; materialization is explicit so
1124    // backend-backed views produce a normal error instead of an implicit path.
1125    let materialized_inputs: Vec<Tensor> = inputs
1126        .iter()
1127        .map(TensorRead::to_tensor)
1128        .collect::<tenferro_tensor::Result<_>>()?;
1129    let input_refs: Vec<&Tensor> = materialized_inputs.iter().collect();
1130    execute_host_fft_op(op, &input_refs)
1131}
1132
1133define_extension_runtime! {
1134    runtime = FftRuntime,
1135    family_id = FFT_EXTENSION_FAMILY_ID,
1136    op_type = FftOp,
1137    execute = execute_fft_extension,
1138    execute_reads = execute_fft_extension_reads,
1139    register_fn = register_runtime,
1140}
1141
1142/// Build a one-dimensional FFT along `axis`.
1143///
1144/// Complex inputs use a complex-to-complex transform. Real inputs use a
1145/// real-to-complex transform that returns the full complex spectrum.
1146///
1147/// # Examples
1148///
1149/// ```
1150/// use num_complex::Complex64;
1151/// use tenferro_cpu::CpuBackend;
1152/// use tenferro_runtime::{GraphCompiler, GraphExecutor, TracedTensor};
1153/// use tenferro_fft::{FftNorm, TracedTensorFftExt};
1154///
1155/// let x = TracedTensor::from_vec_col_major(vec![2], vec![Complex64::new(1.0, 0.0), Complex64::new(2.0, 0.0)]).unwrap();
1156/// let y = x.fft(None, -1, FftNorm::Backward).unwrap();
1157///
1158/// let mut compiler = GraphCompiler::new();
1159/// let program = compiler.compile(&y).unwrap();
1160/// let mut executor = GraphExecutor::new(CpuBackend::new());
1161/// executor.register_extension(tenferro_fft::register_runtime).unwrap();
1162/// let out = executor.run(&program).unwrap();
1163/// assert_eq!(out.as_slice::<Complex64>().unwrap()[0], Complex64::new(3.0, 0.0));
1164/// ```
1165fn fft(input: &TracedTensor, n: Option<usize>, axis: isize, norm: FftNorm) -> Result<TracedTensor> {
1166    let kind = match input.dtype {
1167        DType::C32 | DType::C64 => FftKind::C2C { forward: true },
1168        DType::F32 | DType::F64 => FftKind::R2C { onesided: false },
1169        DType::I32 | DType::I64 | DType::Bool => {
1170            return Err(fft_config_error(
1171                "fft",
1172                format!(
1173                    "fft expects real or complex floating input, got {:?}",
1174                    input.dtype
1175                ),
1176            ))
1177        }
1178    };
1179    apply_unary_fft("fft", input, kind, n, axis, norm)
1180}
1181
1182/// Build a one-dimensional inverse FFT along `axis`.
1183///
1184/// # Examples
1185///
1186/// ```
1187/// use num_complex::Complex64;
1188/// use tenferro_cpu::CpuBackend;
1189/// use tenferro_runtime::{GraphCompiler, GraphExecutor, TracedTensor};
1190/// use tenferro_fft::{FftNorm, TracedTensorFftExt};
1191///
1192/// let spectrum = TracedTensor::from_vec_col_major(vec![2], vec![Complex64::new(3.0, 0.0), Complex64::new(-1.0, 0.0)]).unwrap();
1193/// let y = spectrum.ifft(None, -1, FftNorm::Backward).unwrap();
1194///
1195/// let mut compiler = GraphCompiler::new();
1196/// let program = compiler.compile(&y).unwrap();
1197/// let mut executor = GraphExecutor::new(CpuBackend::new());
1198/// executor.register_extension(tenferro_fft::register_runtime).unwrap();
1199/// let out = executor.run(&program).unwrap();
1200/// assert_eq!(out.as_slice::<Complex64>().unwrap()[0], Complex64::new(1.0, 0.0));
1201/// ```
1202fn ifft(
1203    input: &TracedTensor,
1204    n: Option<usize>,
1205    axis: isize,
1206    norm: FftNorm,
1207) -> Result<TracedTensor> {
1208    if !matches!(input.dtype, DType::C32 | DType::C64) {
1209        return Err(fft_config_error(
1210            "ifft",
1211            format!("ifft expects C32 or C64 input; got {:?}", input.dtype),
1212        ));
1213    }
1214    apply_unary_fft(
1215        "ifft",
1216        input,
1217        FftKind::C2C { forward: false },
1218        n,
1219        axis,
1220        norm,
1221    )
1222}
1223
1224/// Build a one-dimensional real FFT along `axis`.
1225///
1226/// The output keeps only the Hermitian one-sided spectrum with axis length
1227/// `n / 2 + 1`.
1228///
1229/// # Examples
1230///
1231/// ```
1232/// use num_complex::Complex64;
1233/// use tenferro_cpu::CpuBackend;
1234/// use tenferro_runtime::{GraphCompiler, GraphExecutor, TracedTensor};
1235/// use tenferro_fft::{FftNorm, TracedTensorFftExt};
1236///
1237/// let x = TracedTensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap();
1238/// let y = x.rfft(None, -1, FftNorm::Backward).unwrap();
1239///
1240/// let mut compiler = GraphCompiler::new();
1241/// let program = compiler.compile(&y).unwrap();
1242/// let mut executor = GraphExecutor::new(CpuBackend::new());
1243/// executor.register_extension(tenferro_fft::register_runtime).unwrap();
1244/// let out = executor.run(&program).unwrap();
1245/// assert_eq!(out.shape(), &[2]);
1246/// assert_eq!(out.as_slice::<Complex64>().unwrap()[0], Complex64::new(3.0, 0.0));
1247/// ```
1248fn rfft(
1249    input: &TracedTensor,
1250    n: Option<usize>,
1251    axis: isize,
1252    norm: FftNorm,
1253) -> Result<TracedTensor> {
1254    if !matches!(input.dtype, DType::F32 | DType::F64) {
1255        return Err(fft_config_error(
1256            "rfft",
1257            format!("rfft expects F32 or F64 input; got {:?}", input.dtype),
1258        ));
1259    }
1260    apply_unary_fft(
1261        "rfft",
1262        input,
1263        FftKind::R2C { onesided: true },
1264        n,
1265        axis,
1266        norm,
1267    )
1268}
1269
1270/// Build a one-dimensional inverse real FFT along `axis`.
1271///
1272/// If `n` is `None`, the output length is inferred as twice one less than the
1273/// input spectrum length.
1274///
1275/// # Examples
1276///
1277/// ```
1278/// use num_complex::Complex64;
1279/// use tenferro_cpu::CpuBackend;
1280/// use tenferro_runtime::{GraphCompiler, GraphExecutor, TracedTensor};
1281/// use tenferro_fft::{FftNorm, TracedTensorFftExt};
1282///
1283/// let spectrum = TracedTensor::from_vec_col_major(
1284///     vec![2],
1285///     vec![Complex64::new(3.0, 0.0), Complex64::new(-1.0, 0.0)],
1286/// )
1287/// .unwrap();
1288/// let y = spectrum.irfft(Some(2), -1, FftNorm::Backward).unwrap();
1289///
1290/// let mut compiler = GraphCompiler::new();
1291/// let program = compiler.compile(&y).unwrap();
1292/// let mut executor = GraphExecutor::new(CpuBackend::new());
1293/// executor.register_extension(tenferro_fft::register_runtime).unwrap();
1294/// let out = executor.run(&program).unwrap();
1295/// assert_eq!(out.as_slice::<f64>().unwrap(), &[1.0, 2.0]);
1296/// ```
1297fn irfft(
1298    input: &TracedTensor,
1299    n: Option<usize>,
1300    axis: isize,
1301    norm: FftNorm,
1302) -> Result<TracedTensor> {
1303    if !matches!(input.dtype, DType::C32 | DType::C64) {
1304        return Err(fft_config_error(
1305            "irfft",
1306            format!("irfft expects C32 or C64 input; got {:?}", input.dtype),
1307        ));
1308    }
1309    apply_unary_fft("irfft", input, FftKind::C2R, n, axis, norm)
1310}
1311
1312fn apply_unary_fft(
1313    op_name: &'static str,
1314    input: &TracedTensor,
1315    kind: FftKind,
1316    n: Option<usize>,
1317    axis: isize,
1318    norm: FftNorm,
1319) -> Result<TracedTensor> {
1320    validate_n(op_name, n)?;
1321    let axis = normalize_axis(op_name, axis, input.rank)?;
1322    validate_resolved_transform_len(op_name, input, n, axis)?;
1323    if matches!(kind, FftKind::C2R) {
1324        if let Some(shape) = input.try_concrete_shape() {
1325            output_shape_c2r(&shape, axis, n)?;
1326        }
1327    }
1328    let op = Arc::new(FftOp::new(kind, axis, n, norm));
1329    let mut outputs = apply(op, &[input])?;
1330    outputs
1331        .pop()
1332        .ok_or_else(|| Error::Internal("FFT extension declares exactly one output".into()))
1333}
1334
1335fn normalize_axis(op: &'static str, axis: isize, rank: usize) -> Result<usize> {
1336    if rank == 0 {
1337        return Err(fft_config_error(op, "tenferro-fft requires rank >= 1"));
1338    }
1339    let normalized = if axis >= 0 {
1340        axis as usize
1341    } else {
1342        rank.checked_sub(axis.unsigned_abs()).ok_or_else(|| {
1343            fft_config_error(
1344                op,
1345                format!("tenferro-fft axis {axis} out of bounds for rank {rank}"),
1346            )
1347        })?
1348    };
1349    if normalized >= rank {
1350        return Err(fft_config_error(
1351            op,
1352            format!("tenferro-fft axis {axis} out of bounds for rank {rank}"),
1353        ));
1354    }
1355    Ok(normalized)
1356}
1357
1358fn validate_n(op: &'static str, n: Option<usize>) -> Result<()> {
1359    if n == Some(0) {
1360        return Err(fft_config_error(
1361            op,
1362            "tenferro-fft transform length n must be positive",
1363        ));
1364    }
1365    Ok(())
1366}
1367
1368fn validate_resolved_transform_len(
1369    op: &'static str,
1370    input: &TracedTensor,
1371    n: Option<usize>,
1372    axis: usize,
1373) -> Result<()> {
1374    if n.is_some() {
1375        return Ok(());
1376    }
1377    if input
1378        .try_concrete_shape()
1379        .and_then(|shape| shape.get(axis).copied())
1380        == Some(0)
1381    {
1382        return Err(fft_config_error(
1383            op,
1384            "tenferro-fft transform length n must be positive",
1385        ));
1386    }
1387    Ok(())
1388}
1389
1390fn fft_config_error(op: &'static str, message: impl std::fmt::Display) -> Error {
1391    Error::TensorRuntime(tenferro_tensor::Error::InvalidConfig {
1392        op,
1393        message: message.to_string(),
1394    })
1395}
1396
1397fn transform_len_dim(n: Option<usize>, input_dim: &SymDim) -> SymDim {
1398    n.map(SymDim::from).unwrap_or_else(|| input_dim.clone())
1399}
1400
1401fn expected_dtype_for(kind: FftKind) -> DType {
1402    match kind {
1403        FftKind::C2C { .. } | FftKind::C2R => DType::C64,
1404        FftKind::R2C { .. } => DType::F64,
1405    }
1406}
1407
1408fn fft_op_name(kind: FftKind) -> &'static str {
1409    match kind {
1410        FftKind::C2C { forward: true } => "fft",
1411        FftKind::C2C { forward: false } => "ifft",
1412        FftKind::R2C { .. } => "rfft",
1413        FftKind::C2R => "irfft",
1414    }
1415}
1416
1417#[cfg(feature = "autodiff")]
1418fn fft_ad_family_id(kind: FftKind) -> &'static str {
1419    match kind {
1420        FftKind::C2C { .. } => FFT_EXTENSION_FAMILY_ID,
1421        FftKind::R2C { .. } => "tenferro-fft.rfft.v1",
1422        FftKind::C2R => "tenferro-fft.irfft.v1",
1423    }
1424}
1425
1426#[cfg(feature = "autodiff")]
1427fn fft_payload<'a>(op: &'a dyn ExtensionOp, rule: ADRuleKind) -> ADRuleResult<&'a FftOp> {
1428    op.as_any()
1429        .downcast_ref::<FftOp>()
1430        .ok_or_else(|| ADRuleError::unsupported(FFT_EXTENSION_FAMILY_ID, rule))
1431}
1432
1433fn output_shape_c2c(
1434    shape: &[usize],
1435    axis: usize,
1436    n: Option<usize>,
1437) -> tenferro_tensor::Result<Vec<usize>> {
1438    let len = transform_len(shape, axis, n)?;
1439    let mut out_shape = shape.to_vec();
1440    out_shape[axis] = len;
1441    Ok(out_shape)
1442}
1443
1444fn output_shape_r2c(
1445    shape: &[usize],
1446    axis: usize,
1447    n: Option<usize>,
1448    onesided: bool,
1449) -> tenferro_tensor::Result<Vec<usize>> {
1450    let len = transform_len(shape, axis, n)?;
1451    let mut out_shape = shape.to_vec();
1452    out_shape[axis] = if onesided { len / 2 + 1 } else { len };
1453    Ok(out_shape)
1454}
1455
1456fn output_shape_c2r(
1457    shape: &[usize],
1458    axis: usize,
1459    n: Option<usize>,
1460) -> tenferro_tensor::Result<Vec<usize>> {
1461    validate_axis("irfft", shape, axis)?;
1462    let input_len = shape[axis];
1463    let len = match n {
1464        Some(len) => len,
1465        None => default_c2r_output_len(input_len)?,
1466    };
1467    if len == 0 {
1468        return Err(tenferro_tensor::Error::InvalidConfig {
1469            op: "irfft",
1470            message: "output length must be positive".to_string(),
1471        });
1472    }
1473    validate_c2r_spectrum_len(input_len, len)?;
1474    let mut out_shape = shape.to_vec();
1475    out_shape[axis] = len;
1476    Ok(out_shape)
1477}
1478
1479fn output_dim_c2r(input_dim: &SymDim, n: Option<usize>) -> tenferro_tensor::Result<SymDim> {
1480    match (input_dim.constant_value(), n) {
1481        (Some(input_len), Some(output_len)) => {
1482            if output_len == 0 {
1483                return Err(tenferro_tensor::Error::InvalidConfig {
1484                    op: "irfft",
1485                    message: "output length must be positive".to_string(),
1486                });
1487            }
1488            validate_c2r_spectrum_len(input_len, output_len)?;
1489            Ok(SymDim::from(output_len))
1490        }
1491        (Some(input_len), None) => Ok(SymDim::from(default_c2r_output_len(input_len)?)),
1492        (None, Some(output_len)) => {
1493            if output_len == 0 {
1494                return Err(tenferro_tensor::Error::InvalidConfig {
1495                    op: "irfft",
1496                    message: "output length must be positive".to_string(),
1497                });
1498            }
1499            Ok(SymDim::from(output_len))
1500        }
1501        (None, None) => Ok((input_dim.clone() - 1usize) * 2usize),
1502    }
1503}
1504
1505fn default_c2r_output_len(input_len: usize) -> tenferro_tensor::Result<usize> {
1506    if input_len == 0 {
1507        return Err(tenferro_tensor::Error::InvalidConfig {
1508            op: "irfft",
1509            message: "input spectrum axis length must be positive".to_string(),
1510        });
1511    }
1512    input_len
1513        .checked_sub(1)
1514        .and_then(|len| len.checked_mul(2))
1515        .ok_or_else(|| tenferro_tensor::Error::InvalidConfig {
1516            op: "irfft",
1517            message: "default output length overflows usize".to_string(),
1518        })
1519}
1520
1521fn validate_c2r_spectrum_len(
1522    input_len: usize,
1523    output_len: usize,
1524) -> tenferro_tensor::Result<usize> {
1525    let expected = output_len / 2 + 1;
1526    if input_len != expected {
1527        return Err(tenferro_tensor::Error::InvalidConfig {
1528            op: "irfft",
1529            message: format!(
1530                "one-sided spectrum axis length mismatch: expected {expected} for output length {output_len}, got {input_len}"
1531            ),
1532        });
1533    }
1534    Ok(expected)
1535}
1536
1537fn transform_len(shape: &[usize], axis: usize, n: Option<usize>) -> tenferro_tensor::Result<usize> {
1538    validate_axis("fft", shape, axis)?;
1539    let len = n.unwrap_or(shape[axis]);
1540    if len == 0 {
1541        return Err(tenferro_tensor::Error::InvalidConfig {
1542            op: "fft",
1543            message: "transform length must be positive".to_string(),
1544        });
1545    }
1546    Ok(len)
1547}
1548
1549fn validate_axis(op: &'static str, shape: &[usize], axis: usize) -> tenferro_tensor::Result<()> {
1550    if axis >= shape.len() {
1551        return Err(tenferro_tensor::Error::AxisOutOfBounds {
1552            op,
1553            axis,
1554            rank: shape.len(),
1555        });
1556    }
1557    Ok(())
1558}
1559
1560fn checked_shape_product(
1561    op: &'static str,
1562    role: &'static str,
1563    shape: &[usize],
1564) -> tenferro_tensor::Result<usize> {
1565    shape
1566        .iter()
1567        .try_fold(1usize, |acc, &dim| acc.checked_mul(dim))
1568        .ok_or_else(|| tenferro_tensor::Error::InvalidConfig {
1569            op,
1570            message: format!("{role} shape product overflows usize"),
1571        })
1572}
1573
1574fn checked_mul(
1575    op: &'static str,
1576    role: &'static str,
1577    lhs: usize,
1578    rhs: usize,
1579) -> tenferro_tensor::Result<usize> {
1580    lhs.checked_mul(rhs)
1581        .ok_or_else(|| tenferro_tensor::Error::InvalidConfig {
1582            op,
1583            message: format!("{role} overflows usize"),
1584        })
1585}
1586
1587fn checked_add(
1588    op: &'static str,
1589    role: &'static str,
1590    lhs: usize,
1591    rhs: usize,
1592) -> tenferro_tensor::Result<usize> {
1593    lhs.checked_add(rhs)
1594        .ok_or_else(|| tenferro_tensor::Error::InvalidConfig {
1595            op,
1596            message: format!("{role} overflows usize"),
1597        })
1598}
1599
1600fn uninit_output_vec<T>(len: usize) -> Vec<MaybeUninit<T>> {
1601    let mut output = Vec::with_capacity(len);
1602    // SAFETY: Uninitialized bytes are valid for `MaybeUninit<T>` slots. The
1603    // slots are converted to `T` only after all output positions are written.
1604    unsafe { output.set_len(len) };
1605    output
1606}
1607
1608unsafe fn assume_init_output_vec<T>(mut output: Vec<MaybeUninit<T>>) -> Vec<T> {
1609    let len = output.len();
1610    let capacity = output.capacity();
1611    let ptr = output.as_mut_ptr().cast::<T>();
1612    std::mem::forget(output);
1613    // SAFETY: `MaybeUninit<T>` has the same layout as `T`; the caller
1614    // guarantees every slot has been initialized exactly once.
1615    unsafe { Vec::from_raw_parts(ptr, len, capacity) }
1616}
1617
1618#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
1619struct FftPlanKey {
1620    len: usize,
1621    forward: bool,
1622}
1623
1624trait CachedFftPlanScalar: FftNum + Float + FromPrimitive + 'static {
1625    fn cached_fft_plan(len: usize, forward: bool) -> tenferro_tensor::Result<Arc<dyn Fft<Self>>>;
1626}
1627
1628type FftPlanStore<T> = HashMap<FftPlanKey, Arc<dyn Fft<T>>>;
1629type FftPlanCache<T> = OnceLock<Mutex<FftPlanStore<T>>>;
1630
1631static F32_FFT_PLAN_CACHE: FftPlanCache<f32> = OnceLock::new();
1632static F64_FFT_PLAN_CACHE: FftPlanCache<f64> = OnceLock::new();
1633
1634impl CachedFftPlanScalar for f32 {
1635    fn cached_fft_plan(len: usize, forward: bool) -> tenferro_tensor::Result<Arc<dyn Fft<Self>>> {
1636        cached_fft_plan_from_cache(&F32_FFT_PLAN_CACHE, len, forward)
1637    }
1638}
1639
1640impl CachedFftPlanScalar for f64 {
1641    fn cached_fft_plan(len: usize, forward: bool) -> tenferro_tensor::Result<Arc<dyn Fft<Self>>> {
1642        cached_fft_plan_from_cache(&F64_FFT_PLAN_CACHE, len, forward)
1643    }
1644}
1645
1646fn cached_fft_plan<T: CachedFftPlanScalar>(
1647    len: usize,
1648    forward: bool,
1649) -> tenferro_tensor::Result<Arc<dyn Fft<T>>> {
1650    T::cached_fft_plan(len, forward)
1651}
1652
1653fn cached_fft_plan_from_cache<T: FftNum + 'static>(
1654    cache: &'static FftPlanCache<T>,
1655    len: usize,
1656    forward: bool,
1657) -> tenferro_tensor::Result<Arc<dyn Fft<T>>> {
1658    let key = FftPlanKey { len, forward };
1659    let mut guard = cache
1660        .get_or_init(|| Mutex::new(HashMap::new()))
1661        .lock()
1662        .map_err(|_| {
1663            tenferro_tensor::Error::backend_failure(
1664                "fft_plan_cache",
1665                "FFT plan cache lock is poisoned",
1666            )
1667        })?;
1668    if let Some(plan) = guard.get(&key) {
1669        return Ok(Arc::clone(plan));
1670    }
1671
1672    let mut planner = FftPlanner::<T>::new();
1673    let plan = if forward {
1674        planner.plan_fft_forward(len)
1675    } else {
1676        planner.plan_fft_inverse(len)
1677    };
1678    guard.insert(key, Arc::clone(&plan));
1679    Ok(plan)
1680}
1681
1682fn execute_c2c<T>(
1683    input: &TypedTensor<Complex<T>>,
1684    axis: usize,
1685    n: Option<usize>,
1686    forward: bool,
1687    norm: FftNorm,
1688) -> tenferro_tensor::Result<Vec<Complex<T>>>
1689where
1690    T: CachedFftPlanScalar,
1691{
1692    let in_shape = input.shape();
1693    let fft_len = transform_len(in_shape, axis, n)?;
1694    let out_shape = output_shape_c2c(in_shape, axis, n)?;
1695    let out_axis_len = out_shape[axis];
1696    let input_data = input.host_data()?;
1697    let output_len = checked_shape_product("fft", "output", &out_shape)?;
1698    let mut output = uninit_output_vec(output_len);
1699    // Propagate poisoned FFT plan-cache errors to the public caller.
1700    let fft_plan = cached_fft_plan::<T>(fft_len, forward)?;
1701    let scale: T = scale_for(norm, forward, fft_len)?;
1702    let mut lane = vec![Complex::zero(); fft_len];
1703
1704    for_axis_lane(in_shape, axis, out_axis_len, |lane_ctx| {
1705        // INVARIANT: zero-fill is transform padding semantics when the input
1706        // lane is shorter than `fft_len`; it is not redundant initialization.
1707        lane.fill(Complex::zero());
1708        let copy_len = lane_ctx.in_axis_len.min(fft_len);
1709        for (slot, offset) in lane
1710            .iter_mut()
1711            .take(copy_len)
1712            .zip(lane_ctx.input_offsets(copy_len))
1713        {
1714            *slot = input_data[offset];
1715        }
1716        fft_plan.process(&mut lane);
1717        if scale != T::one() {
1718            for value in &mut lane {
1719                *value = *value * scale;
1720            }
1721        }
1722        for (value, offset) in lane
1723            .iter()
1724            .take(out_axis_len)
1725            .copied()
1726            .zip(lane_ctx.output_offsets(out_axis_len))
1727        {
1728            output[offset].write(value);
1729        }
1730        Ok(())
1731    })?;
1732
1733    // SAFETY: `for_axis_lane` covers every element in the compact column-major
1734    // output exactly once, and each lane writes all `out_axis_len` positions.
1735    Ok(unsafe { assume_init_output_vec(output) })
1736}
1737
1738fn execute_r2c<T>(
1739    input: &TypedTensor<T>,
1740    axis: usize,
1741    n: Option<usize>,
1742    onesided: bool,
1743    norm: FftNorm,
1744) -> tenferro_tensor::Result<Vec<Complex<T>>>
1745where
1746    T: CachedFftPlanScalar,
1747{
1748    let in_shape = input.shape();
1749    let fft_len = transform_len(in_shape, axis, n)?;
1750    let out_shape = output_shape_r2c(in_shape, axis, n, onesided)?;
1751    let out_axis_len = out_shape[axis];
1752    let input_data = input.host_data()?;
1753    let output_len = checked_shape_product("rfft", "output", &out_shape)?;
1754    let mut output = uninit_output_vec(output_len);
1755    // Propagate poisoned FFT plan-cache errors to the public caller.
1756    let fft_plan = cached_fft_plan::<T>(fft_len, true)?;
1757    let scale: T = scale_for(norm, true, fft_len)?;
1758    let mut lane = vec![Complex::zero(); fft_len];
1759
1760    for_axis_lane(in_shape, axis, out_axis_len, |lane_ctx| {
1761        // INVARIANT: zero-fill is rfft padding semantics when the real input
1762        // lane is shorter than `fft_len`; later writes cover only `copy_len`.
1763        lane.fill(Complex::zero());
1764        let copy_len = lane_ctx.in_axis_len.min(fft_len);
1765        for (slot, offset) in lane
1766            .iter_mut()
1767            .take(copy_len)
1768            .zip(lane_ctx.input_offsets(copy_len))
1769        {
1770            *slot = Complex::new(input_data[offset], T::zero());
1771        }
1772        fft_plan.process(&mut lane);
1773        if scale != T::one() {
1774            for value in &mut lane {
1775                *value = *value * scale;
1776            }
1777        }
1778        for (value, offset) in lane
1779            .iter()
1780            .take(out_axis_len)
1781            .copied()
1782            .zip(lane_ctx.output_offsets(out_axis_len))
1783        {
1784            output[offset].write(value);
1785        }
1786        Ok(())
1787    })?;
1788
1789    // SAFETY: `for_axis_lane` covers every element in the compact column-major
1790    // output exactly once, and each lane writes all `out_axis_len` positions.
1791    Ok(unsafe { assume_init_output_vec(output) })
1792}
1793
1794fn execute_c2r<T>(
1795    input: &TypedTensor<Complex<T>>,
1796    axis: usize,
1797    n: Option<usize>,
1798    norm: FftNorm,
1799) -> tenferro_tensor::Result<Vec<T>>
1800where
1801    T: CachedFftPlanScalar,
1802{
1803    let in_shape = input.shape();
1804    let out_shape = output_shape_c2r(in_shape, axis, n)?;
1805    let out_axis_len = out_shape[axis];
1806    let expected_half = validate_c2r_spectrum_len(in_shape[axis], out_axis_len)?;
1807    let input_data = input.host_data()?;
1808    let output_len = checked_shape_product("irfft", "output", &out_shape)?;
1809    let mut output = uninit_output_vec(output_len);
1810    // Propagate poisoned FFT plan-cache errors to the public caller.
1811    let fft_plan = cached_fft_plan::<T>(out_axis_len, false)?;
1812    let scale: T = scale_for(norm, false, out_axis_len)?;
1813    let mut lane = vec![Complex::zero(); out_axis_len];
1814
1815    for_axis_lane(in_shape, axis, out_axis_len, |lane_ctx| {
1816        // INVARIANT: zero-fill clears the inverse lane before writing the
1817        // one-sided spectrum and mirrored tail for this lane.
1818        lane.fill(Complex::zero());
1819        for (slot, offset) in lane
1820            .iter_mut()
1821            .take(expected_half)
1822            .zip(lane_ctx.input_offsets(expected_half))
1823        {
1824            *slot = input_data[offset];
1825        }
1826        for k in expected_half..out_axis_len {
1827            let mirror = out_axis_len - k;
1828            if mirror < lane.len() {
1829                lane[k] = lane[mirror].conj();
1830            }
1831        }
1832        fft_plan.process(&mut lane);
1833        for (value, offset) in lane
1834            .iter()
1835            .take(out_axis_len)
1836            .zip(lane_ctx.output_offsets(out_axis_len))
1837        {
1838            output[offset].write(value.re * scale);
1839        }
1840        Ok(())
1841    })?;
1842
1843    // SAFETY: `for_axis_lane` covers every element in the compact column-major
1844    // output exactly once, and each lane writes all `out_axis_len` positions.
1845    Ok(unsafe { assume_init_output_vec(output) })
1846}
1847
1848fn scale_for<T>(norm: FftNorm, forward: bool, n: usize) -> tenferro_tensor::Result<T>
1849where
1850    T: Float + FromPrimitive,
1851{
1852    let len = T::from_usize(n).ok_or_else(|| tenferro_tensor::Error::InvalidConfig {
1853        op: "tenferro_fft::scale_for",
1854        message: format!("FFT length {n} cannot be represented as scalar"),
1855    })?;
1856    Ok(match (norm, forward) {
1857        (FftNorm::Backward, true) | (FftNorm::Forward, false) => T::one(),
1858        (FftNorm::Backward, false) | (FftNorm::Forward, true) => T::one() / len,
1859        (FftNorm::Ortho, _) => T::one() / len.sqrt(),
1860    })
1861}
1862
1863#[derive(Clone, Copy)]
1864struct LaneContext {
1865    input_base: usize,
1866    output_base: usize,
1867    axis_stride: usize,
1868    in_axis_len: usize,
1869    out_axis_len: usize,
1870}
1871
1872impl LaneContext {
1873    fn input_offsets(self, count: usize) -> impl Iterator<Item = usize> {
1874        debug_assert!(count <= self.in_axis_len);
1875        lane_offsets(self.input_base, self.axis_stride, count)
1876    }
1877
1878    fn output_offsets(self, count: usize) -> impl Iterator<Item = usize> {
1879        debug_assert!(count <= self.out_axis_len);
1880        lane_offsets(self.output_base, self.axis_stride, count)
1881    }
1882}
1883
1884fn lane_offsets(base: usize, stride: usize, count: usize) -> impl Iterator<Item = usize> {
1885    // INVARIANT: `for_axis_lane` checks input/output lane coverage before it
1886    // constructs any `LaneContext`, so every `base + k * stride` for
1887    // `k < count` stays within the compact column-major buffer.
1888    (0..count).map(move |k| base + k * stride)
1889}
1890
1891fn for_axis_lane(
1892    in_shape: &[usize],
1893    axis: usize,
1894    out_axis_len: usize,
1895    mut f: impl FnMut(LaneContext) -> tenferro_tensor::Result<()>,
1896) -> tenferro_tensor::Result<()> {
1897    let in_axis_len = in_shape[axis];
1898    let axis_stride = checked_shape_product("fft", "axis stride", &in_shape[..axis])?;
1899    let outer = checked_shape_product("fft", "outer lane count", &in_shape[axis + 1..])?;
1900    let in_block = checked_mul("fft", "input lane block", axis_stride, in_axis_len)?;
1901    let out_block = checked_mul("fft", "output lane block", axis_stride, out_axis_len)?;
1902    let _input_len = checked_mul("fft", "input lane coverage", outer, in_block)?;
1903    let _output_len = checked_mul("fft", "output lane coverage", outer, out_block)?;
1904
1905    // INVARIANT: lanes are processed sequentially so one scratch lane can be
1906    // reused while writing into a single `MaybeUninit` output buffer. Parallel
1907    // lane execution needs disjoint output splitting plus per-worker scratch.
1908    for outer_idx in 0..outer {
1909        let in_outer_base = checked_mul("fft", "input outer base", outer_idx, in_block)?;
1910        let out_outer_base = checked_mul("fft", "output outer base", outer_idx, out_block)?;
1911        for inner in 0..axis_stride {
1912            let input_base = checked_add("fft", "input lane base", in_outer_base, inner)?;
1913            let output_base = checked_add("fft", "output lane base", out_outer_base, inner)?;
1914            f(LaneContext {
1915                input_base,
1916                output_base,
1917                axis_stride,
1918                in_axis_len,
1919                out_axis_len,
1920            })?;
1921        }
1922    }
1923    Ok(())
1924}
1925
1926#[cfg(test)]
1927mod concrete_tests;
1928
1929#[cfg(test)]
1930mod tests {
1931    use super::*;
1932
1933    #[test]
1934    fn fft_infer_output_meta_rejects_invalid_trait_inputs_without_panicking() {
1935        let op = FftOp::new(FftKind::R2C { onesided: true }, 0, None, FftNorm::Backward);
1936        let shape = [SymDim::from(4usize)];
1937
1938        assert!(op.infer_output_meta(&[], &[&shape]).is_err());
1939        assert!(op.infer_output_meta(&[DType::F64], &[]).is_err());
1940        assert!(op.infer_output_meta(&[DType::I64], &[&shape]).is_err());
1941
1942        let bad_axis = FftOp::new(FftKind::C2C { forward: true }, 2, None, FftNorm::Backward);
1943        assert!(bad_axis
1944            .infer_output_meta(&[DType::C64], &[&shape])
1945            .is_err());
1946    }
1947
1948    #[test]
1949    fn checked_shape_product_rejects_overflow_before_allocation() {
1950        let err = checked_shape_product("fft", "output", &[usize::MAX, 2])
1951            .expect_err("overflowing output shape should be rejected");
1952
1953        assert!(err.to_string().contains("overflows usize"), "{err}");
1954    }
1955
1956    #[test]
1957    fn irfft_default_output_length_rejects_overflow() {
1958        let err = output_shape_c2r(&[usize::MAX], 0, None)
1959            .expect_err("default irfft output length should reject overflow");
1960
1961        assert!(err.to_string().contains("overflows usize"), "{err}");
1962    }
1963
1964    #[test]
1965    fn normalize_axis_handles_large_rank_without_isize_cast_wrap() {
1966        assert_eq!(normalize_axis("fft", 0, usize::MAX).unwrap(), 0);
1967        assert_eq!(
1968            normalize_axis("fft", -1, usize::MAX).unwrap(),
1969            usize::MAX - 1
1970        );
1971        assert!(normalize_axis("fft", isize::MIN, 3).is_err());
1972    }
1973
1974    #[test]
1975    fn axis_lane_layout_rejects_stride_overflow() {
1976        let err = for_axis_lane(&[usize::MAX, 2], 1, 2, |_| Ok(()))
1977            .expect_err("lane layout should reject stride overflow");
1978
1979        assert!(err.to_string().contains("overflows usize"), "{err}");
1980    }
1981
1982    #[cfg(feature = "autodiff")]
1983    #[test]
1984    fn fft_transpose_rule_respects_inactive_linearized_input() {
1985        let rule = FftAdRule;
1986        let op = FftOp::new(FftKind::C2C { forward: true }, 0, None, FftNorm::Backward);
1987        let mut builder = computegraph::graph::GraphBuilder::<StdTensorOp>::new();
1988        let cotangent = builder.add_input(tenferro_ops::input_key::TensorInputKey::User { id: 0 });
1989        let result = rule
1990            .linear_transpose(
1991                &op,
1992                &mut builder,
1993                &[Some(cotangent)],
1994                &[],
1995                &[false],
1996                &mut ShapeGuardContext::default(),
1997            )
1998            .unwrap();
1999
2000        assert_eq!(result, vec![None]);
2001        assert!(builder.build().operations().is_empty());
2002    }
2003
2004    #[cfg(feature = "autodiff")]
2005    #[test]
2006    fn fft_transpose_rule_uses_metadata_for_linear_only_matching_length() {
2007        let rule = FftAdRule;
2008        let op = FftOp::new(
2009            FftKind::C2C { forward: true },
2010            0,
2011            Some(4),
2012            FftNorm::Backward,
2013        );
2014        let active_key = ValueKey::Input(tenferro_ops::input_key::TensorInputKey::User { id: 1 });
2015        let mut ctx = ShapeGuardContext::default();
2016        ctx.insert_metadata(
2017            active_key.clone(),
2018            tenferro_ops::TensorMeta::exact(DType::C64, vec![SymDim::from(4usize)]),
2019        );
2020
2021        let mut builder = computegraph::graph::GraphBuilder::<StdTensorOp>::new();
2022        let cotangent = builder.add_input(tenferro_ops::input_key::TensorInputKey::User { id: 0 });
2023        let result = rule
2024            .linear_transpose(
2025                &op,
2026                &mut builder,
2027                &[Some(cotangent)],
2028                &[tidu::PrimitiveTransposeInput::Linear {
2029                    key: active_key.clone(),
2030                    primal: None,
2031                }],
2032                &[true],
2033                &mut ctx,
2034            )
2035            .unwrap();
2036
2037        assert_eq!(result.len(), 1);
2038        assert!(result[0].is_some());
2039        let active_ref = ValueRef::External(active_key);
2040        let graph = builder.build();
2041        assert!(graph
2042            .operations()
2043            .iter()
2044            .all(|node| !node.inputs.iter().any(|input| input == &active_ref)));
2045        assert!(graph.operations().iter().all(|node| {
2046            !matches!(
2047                node.operation,
2048                StdTensorOp::ShapeOf { .. }
2049                    | StdTensorOp::DynamicTruncate { .. }
2050                    | StdTensorOp::PadToMatch { .. }
2051            )
2052        }));
2053    }
2054}