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 with an explicit
4//! [`FftBackend`] capability. [`tenferro_cpu::CpuBackend`] implements the
5//! capability through RustFFT. With the `webgpu` feature,
6//! `tenferro_gpu::WebGpuBackend` executes C32 CFFT, F32 one-sided RFFT, and
7//! C32-to-F32 IRFFT through CubeK on its existing WebGPU placement. That first
8//! GPU path supports power-of-two lengths only; unsupported operations and
9//! dtypes return an error and never fall back to CPU or transfer tensor data.
10//! On macOS, `tenferro_gpu::AppleContext` pairs that Metal backend with a
11//! domain-bound CPU RustFFT backend. Backend choice remains explicit, while
12//! matching managed tensors can be used without an intervening download.
13//! Concrete non-AD execution uses
14//! [`TensorFftExt`] and [`TensorReadFftExt`]. Eager execution uses
15//! `EagerTensorFftExt` when `autodiff` is enabled, and traced graph
16//! construction uses [`TracedTensorFftExt`].
17//!
18//! # Examples
19//!
20//! ```
21//! use num_complex::Complex64;
22//! use tenferro_cpu::CpuBackend;
23//! use tenferro_runtime::{GraphCompiler, Runtime, TracedTensor};
24//! use tenferro_fft::{FftNorm, TracedTensorFftExt};
25//!
26//! let x = TracedTensor::from_vec_col_major(
27//!     vec![4],
28//!     vec![
29//!         Complex64::new(1.0, 0.0),
30//!         Complex64::new(2.0, 0.0),
31//!         Complex64::new(3.0, 0.0),
32//!         Complex64::new(4.0, 0.0),
33//!     ],
34//! )
35//! .unwrap();
36//! let y = x.fft(None, -1, FftNorm::Backward).unwrap();
37//!
38//! let mut compiler = GraphCompiler::new();
39//! let program = compiler.compile(&y).unwrap();
40//! let backend = CpuBackend::new();
41//! let engine_id = tenferro_cpu::runtime_engine_id().unwrap();
42//! let mut builder = Runtime::builder();
43//! builder
44//!     .register_engine(tenferro_cpu::runtime_engine_registration(&backend).unwrap())
45//!     .unwrap();
46//! builder
47//!     .install_extension_module(tenferro_fft::extension_module::<CpuBackend>(engine_id).unwrap())
48//!     .unwrap();
49//! let runtime = builder.build().unwrap();
50//! let out = runtime.run_compiled(&program, &[]).unwrap().pop().unwrap();
51//! assert_eq!(out.shape(), &[4]);
52//! assert_eq!(out.as_slice::<Complex64>().unwrap()[0], Complex64::new(10.0, 0.0));
53//! ```
54//!
55//! ```
56//! # #[cfg(all(feature = "webgpu", target_os = "macos"))]
57//! # {
58//! use num_complex::Complex32;
59//! use tenferro_cpu::with_cpu_exec_session;
60//! use tenferro_fft::{FftNorm, TensorFftExt};
61//! use tenferro_gpu::{with_webgpu_exec_session, AppleContext};
62//! use tenferro_tensor::{BackendSessionHost, Tensor};
63//!
64//! if let Ok(context) = AppleContext::new() {
65//!     let host = Tensor::from_vec_col_major(
66//!         vec![4],
67//!         vec![Complex32::new(1.0, 0.0); 4],
68//!     ).unwrap();
69//!     let input = context.upload_tensor(&host).unwrap();
70//!     let after_creation = context.transfer_stats();
71//!     let mut cpu = context.cpu_backend().clone();
72//!     let cpu_output = cpu
73//!         .with_backend_session(|session| {
74//!             with_cpu_exec_session(session, |exec_session| {
75//!                 input.fft(None, 0, FftNorm::Backward, exec_session)
76//!             })
77//!             .expect("CpuBackend must expose a CPU execution session")
78//!         })
79//!         .unwrap();
80//!     let mut metal = context.metal_backend().clone();
81//!     let output = metal
82//!         .with_backend_session(|session| {
83//!             with_webgpu_exec_session(session, |exec_session| {
84//!                 input.fft(None, 0, FftNorm::Backward, exec_session)
85//!             })
86//!             .expect("WebGpuBackend must expose a WebGPU execution session")
87//!         })
88//!         .unwrap();
89//!     metal
90//!         .with_backend_session(|session| {
91//!             with_webgpu_exec_session(session, |exec_session| {
92//!                 exec_session.runtime().synchronize()
93//!             })
94//!             .expect("WebGpuBackend must expose a WebGPU execution session")
95//!         })
96//!         .unwrap();
97//!     assert_eq!(output.shape(), &[4]);
98//!     assert_eq!(cpu_output.shape(), output.shape());
99//!     assert_eq!(context.transfer_stats(), after_creation);
100//! }
101//! # }
102//! ```
103//!
104//! ```
105//! use num_complex::Complex64;
106//! use tenferro_cpu::{with_cpu_exec_session, CpuBackend};
107//! use tenferro_fft::{FftNorm, TensorFftExt};
108//! use tenferro_tensor::{BackendSessionHost, Tensor};
109//!
110//! let x = Tensor::from_vec_col_major(vec![4], vec![1.0_f64, 2.0, 3.0, 4.0]).unwrap();
111//! let mut backend = CpuBackend::new();
112//! let out = backend
113//!     .with_backend_session(|session| {
114//!         with_cpu_exec_session(session, |exec_session| {
115//!             x.fft(None, -1, FftNorm::Backward, exec_session)
116//!         })
117//!         .expect("CpuBackend must expose a CPU execution session")
118//!     })
119//!     .unwrap();
120//!
121//! assert_eq!(out.as_slice::<Complex64>().unwrap()[0], Complex64::new(10.0, 0.0));
122//! ```
123
124use std::any::Any;
125use std::hash::Hasher;
126use std::num::NonZeroUsize;
127use std::sync::Arc;
128
129#[cfg(feature = "autodiff")]
130use tenferro_ad::semantic_extension::{
131    AdValue, SemanticAdError, SemanticExtensionRegistryError, SemanticExtensionRuleSet,
132    SemanticLinearTransposeRequest, SemanticLinearTransposeRule, SemanticLinearizeRequest,
133    SemanticLinearizeResult, SemanticLinearizeRule, SemanticPrimalVjpRequest,
134    SemanticPrimalVjpRule,
135};
136use tenferro_cpu::with_cpu_exec_session;
137use tenferro_extension_macros::define_extension_runtime;
138#[cfg(feature = "webgpu")]
139use tenferro_gpu::with_webgpu_exec_session;
140use tenferro_ops::SymDim;
141use tenferro_runtime::extension::{
142    apply, ExtensionCacheStore, ExtensionExecutionContext, ExtensionOp,
143};
144#[cfg(feature = "autodiff")]
145use tenferro_runtime::program::{CoreSemanticOp, ProgramValue, SemanticProgramBuilder};
146use tenferro_runtime::{Error, ErrorPhase, Result, TracedTensor};
147use tenferro_tensor::{
148    BackendSession, CacheStats, DType, ErrorKind, Tensor, TensorBackend, TensorRead,
149    ValidationError,
150};
151
152mod backend;
153mod cache;
154mod cpu;
155#[cfg(feature = "autodiff")]
156mod eager_ext;
157mod spec;
158#[cfg(feature = "webgpu")]
159mod webgpu;
160
161pub use backend::{FftBackend, FftExecutionCache};
162pub use cache::{
163    fft_plan_cache_selector, FftPlanCache, DEFAULT_FFT_PLAN_CACHE_CAPACITY, FFT_PLAN_CACHE_NAME,
164};
165#[cfg(feature = "autodiff")]
166pub use eager_ext::EagerTensorFftExt;
167pub use spec::{FftNorm, FftOperation, FftPlanSpec};
168
169/// Extension family id used by the tenferro FFT extension.
170///
171/// # Examples
172///
173/// ```
174/// assert_eq!(
175///     tenferro_fft::FFT_EXTENSION_FAMILY_ID,
176///     "tenferro-fft.fft.v1"
177/// );
178/// ```
179pub const FFT_EXTENSION_FAMILY_ID: &str = "tenferro-fft.fft.v1";
180
181/// Reusable concrete FFT executor with an explicitly owned backend-neutral cache.
182///
183/// Use this executor for repeated concrete FFT calls that should reuse backend
184/// plans. The immediate [`TensorFftExt`] and [`TensorReadFftExt`] methods stay
185/// one-shot and do not retain hidden process-global, thread-local, or
186/// backend-owned plan state between calls.
187#[derive(Default)]
188pub struct FftExecutor {
189    plans: FftPlanCache,
190}
191
192impl FftExecutor {
193    /// Create an executor from a caller-configured FFT execution cache.
194    pub fn new(plans: FftPlanCache) -> Self {
195        Self { plans }
196    }
197
198    /// Inspect the owned backend-neutral FFT cache.
199    pub const fn plan_cache(&self) -> &FftPlanCache {
200        &self.plans
201    }
202
203    /// Mutably inspect or configure the owned backend-neutral FFT cache.
204    pub fn plan_cache_mut(&mut self) -> &mut FftPlanCache {
205        &mut self.plans
206    }
207
208    /// Snapshot aggregate statistics for every backend cache namespace.
209    pub fn cache_stats(&self) -> CacheStats {
210        self.plans.stats()
211    }
212
213    /// Remove every retained backend plan or workspace from this executor.
214    pub fn clear_cache(&mut self) {
215        self.plans.clear();
216    }
217
218    /// Execute a complex or full-spectrum real FFT while reusing owned plans.
219    ///
220    /// # Errors
221    ///
222    /// Returns [`tenferro_tensor::Error::Validation`] with `AxisOutOfBounds` or
223    /// `InvalidArgument` for invalid `axis`/`n`,
224    /// [`tenferro_tensor::Error::Extension`] with [`ErrorKind::Unsupported`]
225    /// for unsupported dtypes, or a typed backend source for execution.
226    pub fn fft<B: FftBackend>(
227        &mut self,
228        input: &Tensor,
229        n: Option<usize>,
230        axis: isize,
231        norm: FftNorm,
232        backend: &mut B,
233    ) -> tenferro_tensor::Result<Tensor> {
234        self.execute(
235            input,
236            concrete_fft_operation("FftExecutor::fft", input.dtype())?,
237            "FftExecutor::fft",
238            n,
239            axis,
240            norm,
241            backend,
242        )
243    }
244
245    /// Execute an inverse complex FFT while reusing owned plans.
246    ///
247    /// # Errors
248    ///
249    /// Returns [`tenferro_tensor::Error::Validation`] with `AxisOutOfBounds` or
250    /// `InvalidArgument` for invalid `axis`/`n`,
251    /// [`tenferro_tensor::Error::Extension`] with [`ErrorKind::Unsupported`]
252    /// for a non-complex input, or a typed backend source for execution.
253    pub fn ifft<B: FftBackend>(
254        &mut self,
255        input: &Tensor,
256        n: Option<usize>,
257        axis: isize,
258        norm: FftNorm,
259        backend: &mut B,
260    ) -> tenferro_tensor::Result<Tensor> {
261        self.execute(
262            input,
263            concrete_ifft_operation("FftExecutor::ifft", input.dtype())?,
264            "FftExecutor::ifft",
265            n,
266            axis,
267            norm,
268            backend,
269        )
270    }
271
272    /// Execute a real FFT while reusing owned plans.
273    ///
274    /// # Errors
275    ///
276    /// Returns [`tenferro_tensor::Error::Validation`] with `AxisOutOfBounds` or
277    /// `InvalidArgument` for invalid `axis`/`n`,
278    /// [`tenferro_tensor::Error::Extension`] with [`ErrorKind::Unsupported`]
279    /// for a non-real input, or a typed backend source for execution.
280    pub fn rfft<B: FftBackend>(
281        &mut self,
282        input: &Tensor,
283        n: Option<usize>,
284        axis: isize,
285        norm: FftNorm,
286        backend: &mut B,
287    ) -> tenferro_tensor::Result<Tensor> {
288        self.execute(
289            input,
290            concrete_rfft_operation("FftExecutor::rfft", input.dtype())?,
291            "FftExecutor::rfft",
292            n,
293            axis,
294            norm,
295            backend,
296        )
297    }
298
299    /// Execute an inverse real FFT while reusing owned plans.
300    ///
301    /// # Errors
302    ///
303    /// Returns [`tenferro_tensor::Error::Validation`] with `AxisOutOfBounds`,
304    /// `InvalidArgument`, or spectrum-length details,
305    /// [`tenferro_tensor::Error::Extension`] with [`ErrorKind::Unsupported`]
306    /// for a non-complex input, or a typed backend source for execution.
307    pub fn irfft<B: FftBackend>(
308        &mut self,
309        input: &Tensor,
310        n: Option<usize>,
311        axis: isize,
312        norm: FftNorm,
313        backend: &mut B,
314    ) -> tenferro_tensor::Result<Tensor> {
315        self.execute(
316            input,
317            concrete_irfft_operation("FftExecutor::irfft", input.dtype())?,
318            "FftExecutor::irfft",
319            n,
320            axis,
321            norm,
322            backend,
323        )
324    }
325
326    #[allow(clippy::too_many_arguments)]
327    fn execute<B: FftBackend>(
328        &mut self,
329        input: &Tensor,
330        operation: FftOperation,
331        op_name: &'static str,
332        n: Option<usize>,
333        axis: isize,
334        norm: FftNorm,
335        backend: &mut B,
336    ) -> tenferro_tensor::Result<Tensor> {
337        let spec = concrete_fft_spec(
338            op_name,
339            operation,
340            input.dtype(),
341            input.shape(),
342            n,
343            axis,
344            norm,
345        )?;
346        backend.execute_fft(
347            input,
348            &spec,
349            FftExecutionCache::caller_owned(&mut self.plans),
350        )
351    }
352}
353
354/// FFT extension methods for [`TracedTensor`].
355pub trait TracedTensorFftExt {
356    /// Build a traced complex or full-spectrum real FFT.
357    ///
358    /// # Errors
359    ///
360    /// Returns `Error::Validation` with `AxisOutOfBounds` or
361    /// `InvalidArgument` for invalid `axis`/`n`, or `Error::Extension` with
362    /// `ErrorKind::Unsupported` for integer, boolean, or otherwise unsupported
363    /// dtypes.
364    ///
365    /// # Deferred errors
366    ///
367    /// Symbolic axis extents and extension execution failures are checked at
368    /// compile or execution time after concrete inputs are bound.
369    fn fft(&self, n: Option<usize>, axis: isize, norm: FftNorm) -> Result<TracedTensor>;
370
371    /// Build a traced inverse complex FFT.
372    ///
373    /// # Errors
374    ///
375    /// Returns `Error::Validation` with `AxisOutOfBounds` or
376    /// `InvalidArgument` for invalid `axis`/`n`, or `Error::Extension` with
377    /// `ErrorKind::Unsupported` when the input is not `C32`/`C64`.
378    ///
379    /// # Deferred errors
380    ///
381    /// Symbolic shape and extension execution failures may be deferred to
382    /// compile or execution.
383    fn ifft(&self, n: Option<usize>, axis: isize, norm: FftNorm) -> Result<TracedTensor>;
384
385    /// Build a traced one-sided real FFT.
386    ///
387    /// # Errors
388    ///
389    /// Returns `Error::Validation` with `AxisOutOfBounds` or
390    /// `InvalidArgument` for invalid `axis`/`n`, or `Error::Extension` with
391    /// `ErrorKind::Unsupported` when the input is not `F32`/`F64`.
392    ///
393    /// # Deferred errors
394    ///
395    /// Symbolic shape and extension execution failures may be deferred to
396    /// compile or execution.
397    fn rfft(&self, n: Option<usize>, axis: isize, norm: FftNorm) -> Result<TracedTensor>;
398
399    /// Build a traced inverse one-sided real FFT.
400    ///
401    /// # Errors
402    ///
403    /// Returns `Error::Validation` with `AxisOutOfBounds` or
404    /// `InvalidArgument` for invalid `axis`/`n` or spectrum length, or
405    /// `Error::Extension` with `ErrorKind::Unsupported` for non-complex input.
406    ///
407    /// # Deferred errors
408    ///
409    /// Symbolic spectrum lengths and extension execution failures may be
410    /// deferred to compile or execution.
411    fn irfft(&self, n: Option<usize>, axis: isize, norm: FftNorm) -> Result<TracedTensor>;
412}
413
414impl TracedTensorFftExt for TracedTensor {
415    fn fft(&self, n: Option<usize>, axis: isize, norm: FftNorm) -> Result<TracedTensor> {
416        fft(self, n, axis, norm)
417    }
418
419    fn ifft(&self, n: Option<usize>, axis: isize, norm: FftNorm) -> Result<TracedTensor> {
420        ifft(self, n, axis, norm)
421    }
422
423    fn rfft(&self, n: Option<usize>, axis: isize, norm: FftNorm) -> Result<TracedTensor> {
424        rfft(self, n, axis, norm)
425    }
426
427    fn irfft(&self, n: Option<usize>, axis: isize, norm: FftNorm) -> Result<TracedTensor> {
428        irfft(self, n, axis, norm)
429    }
430}
431
432/// Backend-explicit FFT methods for concrete [`Tensor`] values.
433///
434/// This is the non-AD immediate execution surface. It uses unsuffixed method
435/// names because the receiver is an owned compact tensor value. Use
436/// [`TensorReadFftExt`] when the input is a borrowed view or other
437/// [`TensorRead`] value.
438///
439/// Direct calls intentionally use a call-local one-shot FFT plan cache. Use
440/// [`FftExecutor`] for repeated concrete calls with stable transform lengths,
441/// or traced/runtime execution when the runtime should own the extension cache.
442///
443/// # Examples
444///
445/// ```
446/// use num_complex::Complex64;
447/// use tenferro_cpu::{with_cpu_exec_session, CpuBackend};
448/// use tenferro_fft::{FftNorm, TensorFftExt};
449/// use tenferro_tensor::{BackendSessionHost, Tensor};
450///
451/// let input = Tensor::from_vec_col_major(vec![4], vec![1.0_f64, 2.0, 3.0, 4.0])?;
452/// let mut backend = CpuBackend::new();
453///
454/// let spectrum = backend.with_backend_session(|session| {
455///     with_cpu_exec_session(session, |exec_session| {
456///         input.fft(None, -1, FftNorm::Backward, exec_session)
457///     })
458///     .expect("CpuBackend must expose a CPU execution session")
459/// })?;
460/// assert_eq!(spectrum.shape(), &[4]);
461/// assert_eq!(spectrum.as_slice::<Complex64>()?[0], Complex64::new(10.0, 0.0));
462/// # Ok::<(), tenferro_tensor::Error>(())
463/// ```
464pub trait TensorFftExt {
465    /// Execute a one-dimensional FFT along `axis`.
466    ///
467    /// # Errors
468    ///
469    /// Returns `Error::Validation` with `AxisOutOfBounds` or `InvalidArgument`
470    /// for `axis`/`n`, `Error::Extension` with `ErrorKind::Unsupported` for an
471    /// integer or boolean input, or a typed backend source for execution.
472    fn fft<B: FftBackend>(
473        &self,
474        n: Option<usize>,
475        axis: isize,
476        norm: FftNorm,
477        backend: &mut B,
478    ) -> tenferro_tensor::Result<Tensor>;
479
480    /// Execute a one-dimensional inverse FFT along `axis`.
481    ///
482    /// # Errors
483    ///
484    /// Returns `Error::Validation` with `AxisOutOfBounds` or `InvalidArgument`
485    /// for `axis`/`n`, `Error::Extension` with `ErrorKind::Unsupported` for a
486    /// non-complex input, or a typed backend source for execution.
487    fn ifft<B: FftBackend>(
488        &self,
489        n: Option<usize>,
490        axis: isize,
491        norm: FftNorm,
492        backend: &mut B,
493    ) -> tenferro_tensor::Result<Tensor>;
494
495    /// Execute a one-dimensional real FFT along `axis`.
496    ///
497    /// # Errors
498    ///
499    /// Returns `Error::Validation` with `AxisOutOfBounds` or `InvalidArgument`
500    /// for `axis`/`n`, `Error::Extension` with `ErrorKind::Unsupported` for a
501    /// non-`F32`/`F64` input, or a typed backend source for execution.
502    fn rfft<B: FftBackend>(
503        &self,
504        n: Option<usize>,
505        axis: isize,
506        norm: FftNorm,
507        backend: &mut B,
508    ) -> tenferro_tensor::Result<Tensor>;
509
510    /// Execute a one-dimensional inverse real FFT along `axis`.
511    ///
512    /// # Errors
513    ///
514    /// Returns `Error::Validation` with `AxisOutOfBounds`, `InvalidArgument`,
515    /// or spectrum-length details, `Error::Extension` with
516    /// `ErrorKind::Unsupported` for a non-complex input, or a typed backend
517    /// source for execution.
518    fn irfft<B: FftBackend>(
519        &self,
520        n: Option<usize>,
521        axis: isize,
522        norm: FftNorm,
523        backend: &mut B,
524    ) -> tenferro_tensor::Result<Tensor>;
525}
526
527impl TensorFftExt for Tensor {
528    fn fft<B: FftBackend>(
529        &self,
530        n: Option<usize>,
531        axis: isize,
532        norm: FftNorm,
533        backend: &mut B,
534    ) -> tenferro_tensor::Result<Tensor> {
535        let spec = concrete_fft_spec(
536            "TensorFftExt::fft",
537            concrete_fft_operation("TensorFftExt::fft", self.dtype())?,
538            self.dtype(),
539            self.shape(),
540            n,
541            axis,
542            norm,
543        )?;
544        execute_concrete_fft_op(self, &spec, backend)
545    }
546
547    fn ifft<B: FftBackend>(
548        &self,
549        n: Option<usize>,
550        axis: isize,
551        norm: FftNorm,
552        backend: &mut B,
553    ) -> tenferro_tensor::Result<Tensor> {
554        let spec = concrete_fft_spec(
555            "TensorFftExt::ifft",
556            concrete_ifft_operation("TensorFftExt::ifft", self.dtype())?,
557            self.dtype(),
558            self.shape(),
559            n,
560            axis,
561            norm,
562        )?;
563        execute_concrete_fft_op(self, &spec, backend)
564    }
565
566    fn rfft<B: FftBackend>(
567        &self,
568        n: Option<usize>,
569        axis: isize,
570        norm: FftNorm,
571        backend: &mut B,
572    ) -> tenferro_tensor::Result<Tensor> {
573        let spec = concrete_fft_spec(
574            "TensorFftExt::rfft",
575            concrete_rfft_operation("TensorFftExt::rfft", self.dtype())?,
576            self.dtype(),
577            self.shape(),
578            n,
579            axis,
580            norm,
581        )?;
582        execute_concrete_fft_op(self, &spec, backend)
583    }
584
585    fn irfft<B: FftBackend>(
586        &self,
587        n: Option<usize>,
588        axis: isize,
589        norm: FftNorm,
590        backend: &mut B,
591    ) -> tenferro_tensor::Result<Tensor> {
592        let spec = concrete_fft_spec(
593            "TensorFftExt::irfft",
594            concrete_irfft_operation("TensorFftExt::irfft", self.dtype())?,
595            self.dtype(),
596            self.shape(),
597            n,
598            axis,
599            norm,
600        )?;
601        execute_concrete_fft_op(self, &spec, backend)
602    }
603}
604
605/// Backend-explicit FFT methods for read-only tensor inputs.
606///
607/// The `_read` suffix follows the repository convention for APIs that
608/// explicitly accept [`TensorRead`] values such as borrowed views.
609///
610/// Direct read calls intentionally materialize through a call-local one-shot
611/// FFT plan cache. Use [`FftExecutor`] on compact owned tensors when repeated
612/// concrete calls should retain backend plans across calls.
613///
614/// # Examples
615///
616/// ```
617/// use num_complex::Complex64;
618/// use tenferro_cpu::{with_cpu_exec_session, CpuBackend};
619/// use tenferro_fft::{FftNorm, TensorReadFftExt};
620/// use tenferro_tensor::{BackendSessionHost, TensorRead, TensorView};
621///
622/// let shape = [4usize];
623/// let data = [1.0_f64, 2.0, 3.0, 4.0];
624/// let input = TensorRead::from_view(TensorView::f64(&shape, &data)?);
625/// let mut backend = CpuBackend::new();
626///
627/// let spectrum = backend.with_backend_session(|session| {
628///     with_cpu_exec_session(session, |exec_session| {
629///         input.fft_read(None, -1, FftNorm::Backward, exec_session)
630///     })
631///     .expect("CpuBackend must expose a CPU execution session")
632/// })?;
633/// assert_eq!(spectrum.as_slice::<Complex64>()?[0], Complex64::new(10.0, 0.0));
634/// # Ok::<(), tenferro_tensor::Error>(())
635/// ```
636pub trait TensorReadFftExt {
637    /// Execute a one-dimensional FFT along `axis`.
638    ///
639    /// # Errors
640    ///
641    /// Returns `Error::Validation` with `AxisOutOfBounds` or `InvalidArgument`
642    /// for `axis`/`n`, `Error::Extension` with `ErrorKind::Unsupported` for an
643    /// integer or boolean input, or a typed backend source for materialization
644    /// or execution.
645    fn fft_read<B: FftBackend>(
646        &self,
647        n: Option<usize>,
648        axis: isize,
649        norm: FftNorm,
650        backend: &mut B,
651    ) -> tenferro_tensor::Result<Tensor>;
652
653    /// Execute a one-dimensional inverse FFT along `axis`.
654    ///
655    /// # Errors
656    ///
657    /// Returns `Error::Validation` with `AxisOutOfBounds` or `InvalidArgument`
658    /// for `axis`/`n`, `Error::Extension` with `ErrorKind::Unsupported` for a
659    /// non-complex input, or a typed backend source for materialization.
660    fn ifft_read<B: FftBackend>(
661        &self,
662        n: Option<usize>,
663        axis: isize,
664        norm: FftNorm,
665        backend: &mut B,
666    ) -> tenferro_tensor::Result<Tensor>;
667
668    /// Execute a one-dimensional real FFT along `axis`.
669    ///
670    /// # Errors
671    ///
672    /// Returns `Error::Validation` with `AxisOutOfBounds` or `InvalidArgument`
673    /// for `axis`/`n`, `Error::Extension` with `ErrorKind::Unsupported` for a
674    /// non-`F32`/`F64` input, or a typed backend source for materialization.
675    fn rfft_read<B: FftBackend>(
676        &self,
677        n: Option<usize>,
678        axis: isize,
679        norm: FftNorm,
680        backend: &mut B,
681    ) -> tenferro_tensor::Result<Tensor>;
682
683    /// Execute a one-dimensional inverse real FFT along `axis`.
684    ///
685    /// # Errors
686    ///
687    /// Returns `Error::Validation` with `AxisOutOfBounds`, `InvalidArgument`,
688    /// or spectrum-length details, `Error::Extension` with
689    /// `ErrorKind::Unsupported` for a non-complex input, or a typed backend
690    /// source for materialization.
691    fn irfft_read<B: FftBackend>(
692        &self,
693        n: Option<usize>,
694        axis: isize,
695        norm: FftNorm,
696        backend: &mut B,
697    ) -> tenferro_tensor::Result<Tensor>;
698}
699
700impl TensorReadFftExt for TensorRead<'_> {
701    fn fft_read<B: FftBackend>(
702        &self,
703        n: Option<usize>,
704        axis: isize,
705        norm: FftNorm,
706        backend: &mut B,
707    ) -> tenferro_tensor::Result<Tensor> {
708        execute_concrete_fft_read_op(
709            self,
710            concrete_fft_operation("TensorReadFftExt::fft_read", self.dtype())?,
711            "TensorReadFftExt::fft_read",
712            n,
713            axis,
714            norm,
715            backend,
716        )
717    }
718
719    fn ifft_read<B: FftBackend>(
720        &self,
721        n: Option<usize>,
722        axis: isize,
723        norm: FftNorm,
724        backend: &mut B,
725    ) -> tenferro_tensor::Result<Tensor> {
726        execute_concrete_fft_read_op(
727            self,
728            concrete_ifft_operation("TensorReadFftExt::ifft_read", self.dtype())?,
729            "TensorReadFftExt::ifft_read",
730            n,
731            axis,
732            norm,
733            backend,
734        )
735    }
736
737    fn rfft_read<B: FftBackend>(
738        &self,
739        n: Option<usize>,
740        axis: isize,
741        norm: FftNorm,
742        backend: &mut B,
743    ) -> tenferro_tensor::Result<Tensor> {
744        execute_concrete_fft_read_op(
745            self,
746            concrete_rfft_operation("TensorReadFftExt::rfft_read", self.dtype())?,
747            "TensorReadFftExt::rfft_read",
748            n,
749            axis,
750            norm,
751            backend,
752        )
753    }
754
755    fn irfft_read<B: FftBackend>(
756        &self,
757        n: Option<usize>,
758        axis: isize,
759        norm: FftNorm,
760        backend: &mut B,
761    ) -> tenferro_tensor::Result<Tensor> {
762        execute_concrete_fft_read_op(
763            self,
764            concrete_irfft_operation("TensorReadFftExt::irfft_read", self.dtype())?,
765            "TensorReadFftExt::irfft_read",
766            n,
767            axis,
768            norm,
769            backend,
770        )
771    }
772}
773
774#[derive(Debug, thiserror::Error)]
775enum FftError {
776    #[error("{op} does not support dtype {dtype:?}; expected {expected}")]
777    UnsupportedDType {
778        op: &'static str,
779        dtype: DType,
780        expected: &'static str,
781    },
782}
783
784#[derive(Clone, Debug, PartialEq)]
785struct FftOp {
786    operation: FftOperation,
787    axis: usize,
788    n: Option<usize>,
789    norm: FftNorm,
790}
791
792impl FftOp {
793    fn new(operation: FftOperation, axis: usize, n: Option<usize>, norm: FftNorm) -> Self {
794        Self {
795            operation,
796            axis,
797            n,
798            norm,
799        }
800    }
801
802    #[cfg(feature = "autodiff")]
803    fn c2c_adjoint(&self) -> Option<Self> {
804        match self.operation {
805            FftOperation::C2cForward => Some(Self {
806                operation: FftOperation::C2cInverse,
807                axis: self.axis,
808                n: self.n,
809                norm: self.norm.c2c_adjoint(),
810            }),
811            FftOperation::C2cInverse => Some(Self {
812                operation: FftOperation::C2cForward,
813                axis: self.axis,
814                n: self.n,
815                norm: self.norm.c2c_adjoint(),
816            }),
817            FftOperation::R2cFull | FftOperation::R2cOnesided | FftOperation::C2r => None,
818        }
819    }
820}
821
822impl ExtensionOp for FftOp {
823    fn family_id(&self) -> &'static str {
824        FFT_EXTENSION_FAMILY_ID
825    }
826
827    fn payload_hash(&self, hasher: &mut dyn Hasher) {
828        let operation = match self.operation {
829            FftOperation::C2cForward => 0,
830            FftOperation::C2cInverse => 1,
831            FftOperation::R2cOnesided => 2,
832            FftOperation::R2cFull => 3,
833            FftOperation::C2r => 4,
834        };
835        hasher.write_u8(operation);
836        hasher.write_usize(self.axis);
837        match self.n {
838            Some(n) => {
839                hasher.write_u8(1);
840                hasher.write_usize(n);
841            }
842            None => hasher.write_u8(0),
843        }
844        let norm = match self.norm {
845            FftNorm::Backward => 0,
846            FftNorm::Forward => 1,
847            FftNorm::Ortho => 2,
848        };
849        hasher.write_u8(norm);
850    }
851
852    fn payload_eq(&self, other: &dyn ExtensionOp) -> bool {
853        other
854            .as_any()
855            .downcast_ref::<FftOp>()
856            .is_some_and(|that| self == that)
857    }
858
859    fn clone_arc(&self) -> Arc<dyn ExtensionOp> {
860        Arc::new(self.clone())
861    }
862
863    fn as_any(&self) -> &dyn Any {
864        self
865    }
866
867    fn input_count(&self) -> usize {
868        1
869    }
870
871    fn output_count(&self) -> usize {
872        1
873    }
874
875    fn semantic_effects(&self) -> tenferro_ops::ext_op::ExtensionEffectDeclaration<'_> {
876        tenferro_ops::ext_op::ExtensionEffectDeclaration::Declared(&[])
877    }
878
879    fn semantic_aliases(&self) -> tenferro_ops::ext_op::ExtensionAliasDeclaration<'_> {
880        tenferro_ops::ext_op::ExtensionAliasDeclaration::AllFresh
881    }
882
883    fn infer_output_meta(
884        &self,
885        ctx: &mut tenferro_ops::ExtensionShapeContext<'_>,
886    ) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
887        let input_dtype = ctx.input_dtype(0)?;
888        let input_shape = ctx.input_shape(0)?;
889        if self.axis >= input_shape.len() {
890            return Err(tenferro_tensor::Error::axis_out_of_bounds(
891                "tenferro-fft",
892                self.axis,
893                input_shape.len(),
894            ));
895        }
896
897        let mut out_shape = input_shape.to_vec();
898        let output_dtype = match self.operation {
899            FftOperation::C2cForward | FftOperation::C2cInverse => {
900                if !matches!(input_dtype, DType::C32 | DType::C64) {
901                    return Err(tensor_unsupported_dtype(
902                        "tenferro-fft",
903                        input_dtype,
904                        "C32 or C64",
905                    ));
906                }
907                input_dtype
908            }
909            FftOperation::R2cFull | FftOperation::R2cOnesided => {
910                let len = transform_len_dim(self.n, &input_shape[self.axis]);
911                out_shape[self.axis] = if self.operation.is_onesided() {
912                    len / 2usize + 1usize
913                } else {
914                    len
915                };
916                match input_dtype {
917                    DType::F32 => DType::C32,
918                    DType::F64 => DType::C64,
919                    _ => {
920                        return Err(tensor_unsupported_dtype(
921                            "tenferro-fft",
922                            input_dtype,
923                            "F32 or F64",
924                        ));
925                    }
926                }
927            }
928            FftOperation::C2r => {
929                out_shape[self.axis] = output_dim_c2r(&input_shape[self.axis], self.n)?;
930                match input_dtype {
931                    DType::C32 => DType::F32,
932                    DType::C64 => DType::F64,
933                    _ => {
934                        return Err(tensor_unsupported_dtype(
935                            "tenferro-fft",
936                            input_dtype,
937                            "C32 or C64",
938                        ));
939                    }
940                }
941            }
942        };
943
944        if self.operation.is_c2c() {
945            out_shape[self.axis] = transform_len_dim(self.n, &input_shape[self.axis]);
946        }
947
948        Ok(vec![(output_dtype, out_shape)])
949    }
950}
951
952fn execute_concrete_fft_op<B: FftBackend>(
953    input: &Tensor,
954    spec: &FftPlanSpec,
955    backend: &mut B,
956) -> tenferro_tensor::Result<Tensor> {
957    let mut plans = FftPlanCache::with_capacity(NonZeroUsize::MIN);
958    backend.execute_fft(input, spec, FftExecutionCache::caller_owned(&mut plans))
959}
960
961#[allow(clippy::too_many_arguments)]
962fn execute_concrete_fft_read_op<B: FftBackend>(
963    input: &TensorRead<'_>,
964    operation: FftOperation,
965    op_name: &'static str,
966    n: Option<usize>,
967    axis: isize,
968    norm: FftNorm,
969    backend: &mut B,
970) -> tenferro_tensor::Result<Tensor> {
971    let spec = concrete_fft_spec(
972        op_name,
973        operation,
974        input.dtype(),
975        input.shape(),
976        n,
977        axis,
978        norm,
979    )?;
980    let materialized = backend.to_contiguous_read(input.clone())?;
981    let mut plans = FftPlanCache::with_capacity(NonZeroUsize::MIN);
982    backend.execute_fft(
983        &materialized,
984        &spec,
985        FftExecutionCache::caller_owned(&mut plans),
986    )
987}
988
989#[allow(clippy::too_many_arguments)]
990fn concrete_fft_spec(
991    op: &'static str,
992    operation: FftOperation,
993    input_dtype: DType,
994    input_shape: &[usize],
995    n: Option<usize>,
996    axis: isize,
997    norm: FftNorm,
998) -> tenferro_tensor::Result<FftPlanSpec> {
999    validate_concrete_n(op, n)?;
1000    let axis = normalize_concrete_axis(op, axis, input_shape.len())?;
1001    validated_fft_plan_spec(op, operation, input_dtype, input_shape, n, axis, norm)
1002}
1003
1004#[allow(clippy::too_many_arguments)]
1005fn validated_fft_plan_spec(
1006    op: &'static str,
1007    operation: FftOperation,
1008    input_dtype: DType,
1009    input_shape: &[usize],
1010    n: Option<usize>,
1011    axis: usize,
1012    norm: FftNorm,
1013) -> tenferro_tensor::Result<FftPlanSpec> {
1014    validate_concrete_n(op, n)?;
1015    validate_operation_dtype(op, operation, input_dtype)?;
1016    validate_axis(op, input_shape, axis)?;
1017    validate_concrete_transform_len(op, input_shape, n, axis)?;
1018    if operation == FftOperation::C2r {
1019        output_shape_c2r(input_shape, axis, n)?;
1020    }
1021    Ok(FftPlanSpec::new(
1022        operation,
1023        axis,
1024        n,
1025        norm,
1026        input_dtype,
1027        input_shape.to_vec(),
1028    ))
1029}
1030
1031fn concrete_fft_operation(op: &'static str, dtype: DType) -> tenferro_tensor::Result<FftOperation> {
1032    match dtype {
1033        DType::C32 | DType::C64 => Ok(FftOperation::C2cForward),
1034        DType::F32 | DType::F64 => Ok(FftOperation::R2cFull),
1035        DType::I32 | DType::I64 | DType::Bool => {
1036            Err(tensor_unsupported_dtype(op, dtype, "F32, F64, C32, or C64"))
1037        }
1038    }
1039}
1040
1041fn concrete_ifft_operation(
1042    op: &'static str,
1043    dtype: DType,
1044) -> tenferro_tensor::Result<FftOperation> {
1045    match dtype {
1046        DType::C32 | DType::C64 => Ok(FftOperation::C2cInverse),
1047        DType::F32 | DType::F64 | DType::I32 | DType::I64 | DType::Bool => {
1048            Err(tensor_unsupported_dtype(op, dtype, "C32 or C64"))
1049        }
1050    }
1051}
1052
1053fn concrete_rfft_operation(
1054    op: &'static str,
1055    dtype: DType,
1056) -> tenferro_tensor::Result<FftOperation> {
1057    match dtype {
1058        DType::F32 | DType::F64 => Ok(FftOperation::R2cOnesided),
1059        DType::C32 | DType::C64 | DType::I32 | DType::I64 | DType::Bool => {
1060            Err(tensor_unsupported_dtype(op, dtype, "F32 or F64"))
1061        }
1062    }
1063}
1064
1065fn concrete_irfft_operation(
1066    op: &'static str,
1067    dtype: DType,
1068) -> tenferro_tensor::Result<FftOperation> {
1069    match dtype {
1070        DType::C32 | DType::C64 => Ok(FftOperation::C2r),
1071        DType::F32 | DType::F64 | DType::I32 | DType::I64 | DType::Bool => {
1072            Err(tensor_unsupported_dtype(op, dtype, "C32 or C64"))
1073        }
1074    }
1075}
1076
1077fn validate_operation_dtype(
1078    op: &'static str,
1079    operation: FftOperation,
1080    dtype: DType,
1081) -> tenferro_tensor::Result<()> {
1082    let supported = match operation {
1083        FftOperation::C2cForward | FftOperation::C2cInverse | FftOperation::C2r => {
1084            matches!(dtype, DType::C32 | DType::C64)
1085        }
1086        FftOperation::R2cFull | FftOperation::R2cOnesided => {
1087            matches!(dtype, DType::F32 | DType::F64)
1088        }
1089    };
1090    if supported {
1091        Ok(())
1092    } else {
1093        Err(tensor_unsupported_dtype(
1094            op,
1095            dtype,
1096            expected_dtype_description(operation),
1097        ))
1098    }
1099}
1100
1101fn validate_concrete_n(op: &'static str, n: Option<usize>) -> tenferro_tensor::Result<()> {
1102    if n == Some(0) {
1103        return Err(tenferro_tensor::Error::invalid_argument(
1104            op,
1105            "n",
1106            "transform length must be positive",
1107        ));
1108    }
1109    Ok(())
1110}
1111
1112fn validate_concrete_transform_len(
1113    op: &'static str,
1114    input_shape: &[usize],
1115    n: Option<usize>,
1116    axis: usize,
1117) -> tenferro_tensor::Result<()> {
1118    if n.is_none() && input_shape.get(axis).copied() == Some(0) {
1119        return Err(tenferro_tensor::Error::invalid_argument(
1120            op,
1121            "n",
1122            "transform length must be positive",
1123        ));
1124    }
1125    Ok(())
1126}
1127
1128fn normalize_concrete_axis(
1129    op: &'static str,
1130    axis: isize,
1131    rank: usize,
1132) -> tenferro_tensor::Result<usize> {
1133    if rank == 0 {
1134        return Err(tenferro_tensor::Error::invalid_argument(
1135            op,
1136            "rank",
1137            "FFT requires rank >= 1",
1138        ));
1139    }
1140    let normalized = if axis >= 0 {
1141        axis as usize
1142    } else {
1143        rank.checked_sub(axis.unsigned_abs()).ok_or_else(|| {
1144            tenferro_tensor::Error::axis_out_of_bounds(op, axis.unsigned_abs(), rank)
1145        })?
1146    };
1147    if normalized >= rank {
1148        return Err(tenferro_tensor::Error::axis_out_of_bounds(
1149            op, normalized, rank,
1150        ));
1151    }
1152    Ok(normalized)
1153}
1154
1155fn tensor_unsupported_dtype(
1156    op: &'static str,
1157    dtype: DType,
1158    expected: &'static str,
1159) -> tenferro_tensor::Error {
1160    tenferro_tensor::Error::extension(
1161        op,
1162        FFT_EXTENSION_FAMILY_ID,
1163        ErrorKind::Unsupported,
1164        FftError::UnsupportedDType {
1165            op,
1166            dtype,
1167            expected,
1168        },
1169    )
1170}
1171
1172#[cfg(feature = "autodiff")]
1173#[derive(Debug)]
1174struct FftAdRule;
1175
1176#[cfg(feature = "autodiff")]
1177impl SemanticLinearizeRule for FftAdRule {
1178    fn family_id(&self) -> &'static str {
1179        FFT_EXTENSION_FAMILY_ID
1180    }
1181
1182    fn linearize(
1183        &self,
1184        request: SemanticLinearizeRequest<'_>,
1185        builder: &mut SemanticProgramBuilder,
1186    ) -> std::result::Result<SemanticLinearizeResult, SemanticAdError> {
1187        let fft_op = semantic_fft_payload(request.op(), SemanticAdRuleKind::Linearize)?;
1188        if !fft_op.operation.is_c2c() {
1189            return Err(semantic_fft_unsupported(
1190                fft_op.operation,
1191                SemanticAdRuleKind::Linearize,
1192            ));
1193        }
1194        let tangent = match request.tangent_inputs()[0] {
1195            AdValue::Absent => AdValue::Absent,
1196            AdValue::Value(tangent) => {
1197                AdValue::Value(builder.add_extension(Arc::new(fft_op.clone()), &[tangent])?[0])
1198            }
1199        };
1200        Ok(SemanticLinearizeResult::new([tangent], []))
1201    }
1202}
1203
1204#[cfg(feature = "autodiff")]
1205impl SemanticLinearTransposeRule for FftAdRule {
1206    fn family_id(&self) -> &'static str {
1207        FFT_EXTENSION_FAMILY_ID
1208    }
1209
1210    fn linear_transpose(
1211        &self,
1212        request: SemanticLinearTransposeRequest<'_>,
1213        builder: &mut SemanticProgramBuilder,
1214    ) -> std::result::Result<Box<[AdValue]>, SemanticAdError> {
1215        Ok([semantic_fft_adjoint(
1216            request.op(),
1217            request.cotangent_outputs()[0],
1218            request.active_inputs()[0],
1219            request.primal_inputs()[0],
1220            builder,
1221        )?]
1222        .into())
1223    }
1224}
1225
1226#[cfg(feature = "autodiff")]
1227impl SemanticPrimalVjpRule for FftAdRule {
1228    fn family_id(&self) -> &'static str {
1229        FFT_EXTENSION_FAMILY_ID
1230    }
1231
1232    fn primal_vjp(
1233        &self,
1234        request: SemanticPrimalVjpRequest<'_>,
1235        builder: &mut SemanticProgramBuilder,
1236    ) -> std::result::Result<Box<[AdValue]>, SemanticAdError> {
1237        Ok([semantic_fft_adjoint(
1238            request.op(),
1239            request.cotangent_outputs()[0],
1240            request.active_inputs()[0],
1241            request.primal_inputs()[0],
1242            builder,
1243        )?]
1244        .into())
1245    }
1246}
1247
1248#[cfg(feature = "autodiff")]
1249#[derive(Clone, Copy)]
1250enum SemanticAdRuleKind {
1251    Linearize,
1252    Transpose,
1253}
1254
1255#[cfg(feature = "autodiff")]
1256fn semantic_fft_payload(
1257    op: &dyn ExtensionOp,
1258    role: SemanticAdRuleKind,
1259) -> std::result::Result<&FftOp, SemanticAdError> {
1260    op.as_any().downcast_ref::<FftOp>().ok_or_else(|| {
1261        semantic_fft_unsupported_family(
1262            FFT_EXTENSION_FAMILY_ID,
1263            role,
1264            "FFT semantic AD received an incompatible extension payload",
1265        )
1266    })
1267}
1268
1269#[cfg(feature = "autodiff")]
1270fn semantic_fft_adjoint(
1271    op: &dyn ExtensionOp,
1272    cotangent: AdValue,
1273    active: bool,
1274    primal_input: ProgramValue,
1275    builder: &mut SemanticProgramBuilder,
1276) -> std::result::Result<AdValue, SemanticAdError> {
1277    if !active {
1278        return Ok(AdValue::Absent);
1279    }
1280    let AdValue::Value(cotangent) = cotangent else {
1281        return Ok(AdValue::Absent);
1282    };
1283    let fft_op = semantic_fft_payload(op, SemanticAdRuleKind::Transpose)?;
1284    if !fft_op.operation.is_c2c() {
1285        return Err(semantic_fft_unsupported(
1286            fft_op.operation,
1287            SemanticAdRuleKind::Transpose,
1288        ));
1289    }
1290    let adjoint_op = fft_op
1291        .c2c_adjoint()
1292        .ok_or_else(|| semantic_fft_unsupported(fft_op.operation, SemanticAdRuleKind::Transpose))?;
1293    let adjoint = builder.add_extension(Arc::new(adjoint_op), &[cotangent])?[0];
1294    restore_semantic_c2c_adjoint_input_length(builder, adjoint, primal_input, fft_op)
1295        .map(AdValue::Value)
1296}
1297
1298#[cfg(feature = "autodiff")]
1299fn restore_semantic_c2c_adjoint_input_length(
1300    builder: &mut SemanticProgramBuilder,
1301    adjoint: ProgramValue,
1302    primal_input: ProgramValue,
1303    fft_op: &FftOp,
1304) -> std::result::Result<ProgramValue, SemanticAdError> {
1305    let Some(transform_len) = fft_op.n else {
1306        return Ok(adjoint);
1307    };
1308    let input_len = builder
1309        .value_metadata(primal_input)?
1310        .shape()
1311        .get(fft_op.axis)
1312        .and_then(|extent| extent.as_exact())
1313        .and_then(|dim| match dim {
1314            tenferro_ops::dim_expr::DimExpr::Const(value) => Some(*value),
1315            _ => None,
1316        });
1317    if input_len == Some(transform_len) {
1318        return Ok(adjoint);
1319    }
1320
1321    let size = builder.add_op(
1322        CoreSemanticOp::ShapeOf { axis: fft_op.axis },
1323        &[primal_input],
1324    )?[0];
1325    let truncated = builder.add_op(
1326        CoreSemanticOp::DynamicTruncate { axis: fft_op.axis },
1327        &[adjoint, size],
1328    )?[0];
1329    Ok(builder.add_op(
1330        CoreSemanticOp::PadToMatch { axis: fft_op.axis },
1331        &[truncated, primal_input],
1332    )?[0])
1333}
1334
1335#[cfg(feature = "autodiff")]
1336fn semantic_fft_unsupported(operation: FftOperation, role: SemanticAdRuleKind) -> SemanticAdError {
1337    semantic_fft_unsupported_family(
1338        fft_ad_family_id(operation),
1339        role,
1340        "FFT operation has no semantic AD rule",
1341    )
1342}
1343
1344#[cfg(feature = "autodiff")]
1345fn semantic_fft_unsupported_family(
1346    family_id: &'static str,
1347    role: SemanticAdRuleKind,
1348    message: impl Into<String>,
1349) -> SemanticAdError {
1350    SemanticAdError::Unsupported {
1351        family_id,
1352        role: match role {
1353            SemanticAdRuleKind::Linearize => {
1354                tenferro_ad::semantic_extension::SemanticAdRuleRole::Linearize
1355            }
1356            SemanticAdRuleKind::Transpose => {
1357                tenferro_ad::semantic_extension::SemanticAdRuleRole::LinearTranspose
1358            }
1359        },
1360        message: message.into(),
1361    }
1362}
1363
1364/// Return the semantic-program FFT extension AD rule set.
1365#[cfg(feature = "autodiff")]
1366///
1367/// # Errors
1368///
1369/// Returns [`SemanticExtensionRegistryError::MalformedFamilyId`] if the FFT
1370/// family identifier is invalid, or
1371/// [`SemanticExtensionRegistryError::DuplicateRule`] if a rule for the family
1372/// and role is already registered.
1373pub fn semantic_ad_rules(
1374) -> std::result::Result<SemanticExtensionRuleSet, SemanticExtensionRegistryError> {
1375    SemanticExtensionRuleSet::new()
1376        .with_linearize(Arc::new(FftAdRule))?
1377        .with_linear_transpose(Arc::new(FftAdRule))?
1378        .with_primal_vjp(Arc::new(FftAdRule))
1379}
1380
1381pub(crate) fn execute_fft_extension_reads_owner<B: TensorBackend + 'static>(
1382    op: &FftOp,
1383    inputs: &[TensorRead<'_>],
1384    ctx: &mut ExtensionExecutionContext<'_, B>,
1385) -> tenferro_tensor::Result<Vec<Tensor>> {
1386    let (backend, caches) = ctx.parts_mut();
1387    backend.with_backend_session(|session| {
1388        execute_fft_extension_reads_on_session(op, inputs, session, caches)
1389    })
1390}
1391
1392#[cfg(feature = "autodiff")]
1393pub(crate) fn execute_fft_extension_reads_session(
1394    op: &FftOp,
1395    inputs: &[TensorRead<'_>],
1396    ctx: &mut ExtensionExecutionContext<'_, dyn BackendSession + '_>,
1397) -> tenferro_tensor::Result<Vec<Tensor>> {
1398    let (session, caches) = ctx.parts_mut();
1399    execute_fft_extension_reads_on_session(op, inputs, session, caches)
1400}
1401
1402fn execute_fft_extension_for_capability<B: FftBackend + ?Sized>(
1403    op: &FftOp,
1404    inputs: &[&Tensor],
1405    session: &mut B,
1406    caches: &mut ExtensionCacheStore,
1407) -> tenferro_tensor::Result<Vec<Tensor>> {
1408    if inputs.len() != 1 {
1409        return Err(tenferro_tensor::Error::invalid_argument(
1410            "tenferro-fft",
1411            "inputs",
1412            format!("expected 1 input, got {}", inputs.len()),
1413        ));
1414    }
1415    let input = inputs[0];
1416    let spec = validated_fft_plan_spec(
1417        fft_op_name(op.operation),
1418        op.operation,
1419        input.dtype(),
1420        input.shape(),
1421        op.n,
1422        op.axis,
1423        op.norm,
1424    )?;
1425    let output = session.execute_fft(input, &spec, FftExecutionCache::runtime_owned(caches))?;
1426    Ok(vec![output])
1427}
1428
1429fn execute_fft_extension_reads_on_session(
1430    op: &FftOp,
1431    inputs: &[TensorRead<'_>],
1432    session: &mut dyn BackendSession,
1433    caches: &mut ExtensionCacheStore,
1434) -> tenferro_tensor::Result<Vec<Tensor>> {
1435    if let Some(result) = with_cpu_exec_session(session, |session| {
1436        execute_fft_extension_reads_for_capability(op, inputs, session, caches)
1437    }) {
1438        return result;
1439    }
1440    #[cfg(feature = "webgpu")]
1441    if let Some(result) = with_webgpu_exec_session(session, |session| {
1442        execute_fft_extension_reads_for_capability(op, inputs, session, caches)
1443    }) {
1444        return result;
1445    }
1446    Err(tenferro_tensor::Error::unsupported(
1447        fft_op_name(op.operation),
1448        "selected backend session does not expose an FFT execution capability",
1449    ))
1450}
1451
1452fn execute_fft_extension_reads_for_capability<B: FftBackend + ?Sized>(
1453    op: &FftOp,
1454    inputs: &[TensorRead<'_>],
1455    session: &mut B,
1456    caches: &mut ExtensionCacheStore,
1457) -> tenferro_tensor::Result<Vec<Tensor>> {
1458    let op_name = fft_op_name(op.operation);
1459    for input in inputs {
1460        session.validate_fft_read_input(op_name, input)?;
1461    }
1462    let materialized_inputs = inputs
1463        .iter()
1464        .cloned()
1465        .map(|input| session.to_contiguous_read(input))
1466        .collect::<tenferro_tensor::Result<Vec<_>>>()?;
1467    let input_refs: Vec<&Tensor> = materialized_inputs.iter().collect();
1468    execute_fft_extension_for_capability(op, &input_refs, session, caches)
1469}
1470
1471define_extension_runtime! {
1472    runtime = FftRuntime,
1473    family_id = FFT_EXTENSION_FAMILY_ID,
1474    op_type = FftOp,
1475    execute = execute_fft_extension_reads_owner,
1476    execute_reads = execute_fft_extension_reads_owner,
1477    backend_bound = TensorBackend,
1478}
1479
1480/// Build a one-dimensional FFT along `axis`.
1481///
1482/// Complex inputs use a complex-to-complex transform. Real inputs use a
1483/// real-to-complex transform that returns the full complex spectrum.
1484///
1485/// # Examples
1486///
1487/// ```
1488/// use num_complex::Complex64;
1489/// use tenferro_cpu::CpuBackend;
1490/// use tenferro_runtime::{GraphCompiler, Runtime, TracedTensor};
1491/// use tenferro_fft::{FftNorm, TracedTensorFftExt};
1492///
1493/// let x = TracedTensor::from_vec_col_major(vec![2], vec![Complex64::new(1.0, 0.0), Complex64::new(2.0, 0.0)]).unwrap();
1494/// let y = x.fft(None, -1, FftNorm::Backward).unwrap();
1495///
1496/// let mut compiler = GraphCompiler::new();
1497/// let program = compiler.compile(&y).unwrap();
1498/// let backend = CpuBackend::new();
1499/// let engine_id = tenferro_cpu::runtime_engine_id().unwrap();
1500/// let mut builder = Runtime::builder();
1501/// builder
1502///     .register_engine(tenferro_cpu::runtime_engine_registration(&backend).unwrap())
1503///     .unwrap();
1504/// builder
1505///     .install_extension_module(tenferro_fft::extension_module::<CpuBackend>(engine_id).unwrap())
1506///     .unwrap();
1507/// let runtime = builder.build().unwrap();
1508/// let out = runtime.run_compiled(&program, &[]).unwrap().pop().unwrap();
1509/// assert_eq!(out.as_slice::<Complex64>().unwrap()[0], Complex64::new(3.0, 0.0));
1510/// ```
1511fn fft(input: &TracedTensor, n: Option<usize>, axis: isize, norm: FftNorm) -> Result<TracedTensor> {
1512    let operation = runtime_forward_fft_operation(input.dtype)?;
1513    apply_unary_fft("fft", input, operation, n, axis, norm)
1514}
1515
1516/// Build a one-dimensional inverse FFT along `axis`.
1517///
1518/// # Examples
1519///
1520/// ```
1521/// use num_complex::Complex64;
1522/// use tenferro_cpu::CpuBackend;
1523/// use tenferro_runtime::{GraphCompiler, Runtime, TracedTensor};
1524/// use tenferro_fft::{FftNorm, TracedTensorFftExt};
1525///
1526/// let spectrum = TracedTensor::from_vec_col_major(vec![2], vec![Complex64::new(3.0, 0.0), Complex64::new(-1.0, 0.0)]).unwrap();
1527/// let y = spectrum.ifft(None, -1, FftNorm::Backward).unwrap();
1528///
1529/// let mut compiler = GraphCompiler::new();
1530/// let program = compiler.compile(&y).unwrap();
1531/// let backend = CpuBackend::new();
1532/// let engine_id = tenferro_cpu::runtime_engine_id().unwrap();
1533/// let mut builder = Runtime::builder();
1534/// builder
1535///     .register_engine(tenferro_cpu::runtime_engine_registration(&backend).unwrap())
1536///     .unwrap();
1537/// builder
1538///     .install_extension_module(tenferro_fft::extension_module::<CpuBackend>(engine_id).unwrap())
1539///     .unwrap();
1540/// let runtime = builder.build().unwrap();
1541/// let out = runtime.run_compiled(&program, &[]).unwrap().pop().unwrap();
1542/// assert_eq!(out.as_slice::<Complex64>().unwrap()[0], Complex64::new(1.0, 0.0));
1543/// ```
1544fn ifft(
1545    input: &TracedTensor,
1546    n: Option<usize>,
1547    axis: isize,
1548    norm: FftNorm,
1549) -> Result<TracedTensor> {
1550    require_runtime_dtype("ifft", input.dtype, &[DType::C32, DType::C64], "C32 or C64")?;
1551    apply_unary_fft("ifft", input, FftOperation::C2cInverse, n, axis, norm)
1552}
1553
1554/// Build a one-dimensional real FFT along `axis`.
1555///
1556/// The output keeps only the Hermitian one-sided spectrum with axis length
1557/// `n / 2 + 1`.
1558///
1559/// # Examples
1560///
1561/// ```
1562/// use num_complex::Complex64;
1563/// use tenferro_cpu::CpuBackend;
1564/// use tenferro_runtime::{GraphCompiler, Runtime, TracedTensor};
1565/// use tenferro_fft::{FftNorm, TracedTensorFftExt};
1566///
1567/// let x = TracedTensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap();
1568/// let y = x.rfft(None, -1, FftNorm::Backward).unwrap();
1569///
1570/// let mut compiler = GraphCompiler::new();
1571/// let program = compiler.compile(&y).unwrap();
1572/// let backend = CpuBackend::new();
1573/// let engine_id = tenferro_cpu::runtime_engine_id().unwrap();
1574/// let mut builder = Runtime::builder();
1575/// builder
1576///     .register_engine(tenferro_cpu::runtime_engine_registration(&backend).unwrap())
1577///     .unwrap();
1578/// builder
1579///     .install_extension_module(tenferro_fft::extension_module::<CpuBackend>(engine_id).unwrap())
1580///     .unwrap();
1581/// let runtime = builder.build().unwrap();
1582/// let out = runtime.run_compiled(&program, &[]).unwrap().pop().unwrap();
1583/// assert_eq!(out.shape(), &[2]);
1584/// assert_eq!(out.as_slice::<Complex64>().unwrap()[0], Complex64::new(3.0, 0.0));
1585/// ```
1586fn rfft(
1587    input: &TracedTensor,
1588    n: Option<usize>,
1589    axis: isize,
1590    norm: FftNorm,
1591) -> Result<TracedTensor> {
1592    require_runtime_dtype("rfft", input.dtype, &[DType::F32, DType::F64], "F32 or F64")?;
1593    apply_unary_fft("rfft", input, FftOperation::R2cOnesided, n, axis, norm)
1594}
1595
1596/// Build a one-dimensional inverse real FFT along `axis`.
1597///
1598/// If `n` is `None`, the output length is inferred as twice one less than the
1599/// input spectrum length.
1600///
1601/// # Examples
1602///
1603/// ```
1604/// use num_complex::Complex64;
1605/// use tenferro_cpu::CpuBackend;
1606/// use tenferro_runtime::{GraphCompiler, Runtime, TracedTensor};
1607/// use tenferro_fft::{FftNorm, TracedTensorFftExt};
1608///
1609/// let spectrum = TracedTensor::from_vec_col_major(
1610///     vec![2],
1611///     vec![Complex64::new(3.0, 0.0), Complex64::new(-1.0, 0.0)],
1612/// )
1613/// .unwrap();
1614/// let y = spectrum.irfft(Some(2), -1, FftNorm::Backward).unwrap();
1615///
1616/// let mut compiler = GraphCompiler::new();
1617/// let program = compiler.compile(&y).unwrap();
1618/// let backend = CpuBackend::new();
1619/// let engine_id = tenferro_cpu::runtime_engine_id().unwrap();
1620/// let mut builder = Runtime::builder();
1621/// builder
1622///     .register_engine(tenferro_cpu::runtime_engine_registration(&backend).unwrap())
1623///     .unwrap();
1624/// builder
1625///     .install_extension_module(tenferro_fft::extension_module::<CpuBackend>(engine_id).unwrap())
1626///     .unwrap();
1627/// let runtime = builder.build().unwrap();
1628/// let out = runtime.run_compiled(&program, &[]).unwrap().pop().unwrap();
1629/// assert_eq!(out.as_slice::<f64>().unwrap(), &[1.0, 2.0]);
1630/// ```
1631fn irfft(
1632    input: &TracedTensor,
1633    n: Option<usize>,
1634    axis: isize,
1635    norm: FftNorm,
1636) -> Result<TracedTensor> {
1637    require_runtime_dtype(
1638        "irfft",
1639        input.dtype,
1640        &[DType::C32, DType::C64],
1641        "C32 or C64",
1642    )?;
1643    apply_unary_fft("irfft", input, FftOperation::C2r, n, axis, norm)
1644}
1645
1646fn apply_unary_fft(
1647    op_name: &'static str,
1648    input: &TracedTensor,
1649    operation: FftOperation,
1650    n: Option<usize>,
1651    axis: isize,
1652    norm: FftNorm,
1653) -> Result<TracedTensor> {
1654    let concrete_shape = input.try_concrete_shape();
1655    let op = Arc::new(prepare_runtime_fft_op(
1656        op_name,
1657        operation,
1658        input.rank,
1659        concrete_shape.as_deref(),
1660        n,
1661        axis,
1662        norm,
1663    )?);
1664    let mut outputs = apply(op, &[input])?;
1665    outputs
1666        .pop()
1667        .ok_or_else(|| Error::Internal("FFT extension declares exactly one output".into()))
1668}
1669
1670fn normalize_axis(op: &'static str, axis: isize, rank: usize) -> Result<usize> {
1671    if rank == 0 {
1672        return Err(runtime_invalid_argument(
1673            op,
1674            "rank",
1675            "FFT requires rank >= 1",
1676        ));
1677    }
1678    let normalized = if axis >= 0 {
1679        axis as usize
1680    } else {
1681        rank.checked_sub(axis.unsigned_abs())
1682            .ok_or_else(|| runtime_axis_out_of_bounds(op, axis.unsigned_abs(), rank))?
1683    };
1684    if normalized >= rank {
1685        return Err(runtime_axis_out_of_bounds(op, normalized, rank));
1686    }
1687    Ok(normalized)
1688}
1689
1690fn validate_n(op: &'static str, n: Option<usize>) -> Result<()> {
1691    if n == Some(0) {
1692        return Err(runtime_invalid_argument(
1693            op,
1694            "n",
1695            "transform length must be positive",
1696        ));
1697    }
1698    Ok(())
1699}
1700
1701fn prepare_runtime_fft_op(
1702    op: &'static str,
1703    operation: FftOperation,
1704    rank: usize,
1705    concrete_shape: Option<&[usize]>,
1706    n: Option<usize>,
1707    axis: isize,
1708    norm: FftNorm,
1709) -> Result<FftOp> {
1710    validate_n(op, n)?;
1711    let axis = normalize_axis(op, axis, rank)?;
1712    if n.is_none() && concrete_shape.and_then(|shape| shape.get(axis).copied()) == Some(0) {
1713        return Err(runtime_invalid_argument(
1714            op,
1715            "n",
1716            "transform length must be positive",
1717        ));
1718    }
1719    if operation == FftOperation::C2r {
1720        if let Some(shape) = concrete_shape {
1721            output_shape_c2r(shape, axis, n)?;
1722        }
1723    }
1724    Ok(FftOp::new(operation, axis, n, norm))
1725}
1726
1727fn runtime_forward_fft_operation(dtype: DType) -> Result<FftOperation> {
1728    match dtype {
1729        DType::C32 | DType::C64 => Ok(FftOperation::C2cForward),
1730        DType::F32 | DType::F64 => Ok(FftOperation::R2cFull),
1731        DType::I32 | DType::I64 | DType::Bool => Err(runtime_unsupported_dtype(
1732            "fft",
1733            dtype,
1734            "F32, F64, C32, or C64",
1735        )),
1736    }
1737}
1738
1739fn require_runtime_dtype(
1740    op: &'static str,
1741    dtype: DType,
1742    supported: &[DType],
1743    expected: &'static str,
1744) -> Result<()> {
1745    if supported.contains(&dtype) {
1746        Ok(())
1747    } else {
1748        Err(runtime_unsupported_dtype(op, dtype, expected))
1749    }
1750}
1751
1752fn runtime_invalid_argument(
1753    op: &'static str,
1754    argument: &'static str,
1755    message: impl Into<String>,
1756) -> Error {
1757    Error::validation(
1758        op,
1759        ErrorPhase::GraphBuild,
1760        ValidationError::InvalidArgument {
1761            argument,
1762            message: message.into(),
1763        },
1764    )
1765}
1766
1767fn runtime_axis_out_of_bounds(op: &'static str, axis: usize, rank: usize) -> Error {
1768    Error::validation(
1769        op,
1770        ErrorPhase::GraphBuild,
1771        ValidationError::AxisOutOfBounds { axis, rank },
1772    )
1773}
1774
1775fn runtime_unsupported_dtype(op: &'static str, dtype: DType, expected: &'static str) -> Error {
1776    Error::extension(
1777        op,
1778        ErrorPhase::GraphBuild,
1779        FFT_EXTENSION_FAMILY_ID,
1780        ErrorKind::Unsupported,
1781        FftError::UnsupportedDType {
1782            op,
1783            dtype,
1784            expected,
1785        },
1786    )
1787}
1788
1789fn transform_len_dim(n: Option<usize>, input_dim: &SymDim) -> SymDim {
1790    n.map(SymDim::from).unwrap_or_else(|| input_dim.clone())
1791}
1792
1793fn expected_dtype_description(operation: FftOperation) -> &'static str {
1794    match operation {
1795        FftOperation::C2cForward | FftOperation::C2cInverse | FftOperation::C2r => "C32 or C64",
1796        FftOperation::R2cFull | FftOperation::R2cOnesided => "F32 or F64",
1797    }
1798}
1799
1800fn fft_op_name(operation: FftOperation) -> &'static str {
1801    match operation {
1802        FftOperation::C2cForward => "fft",
1803        FftOperation::C2cInverse => "ifft",
1804        FftOperation::R2cFull | FftOperation::R2cOnesided => "rfft",
1805        FftOperation::C2r => "irfft",
1806    }
1807}
1808
1809#[cfg(feature = "autodiff")]
1810fn fft_ad_family_id(operation: FftOperation) -> &'static str {
1811    match operation {
1812        FftOperation::C2cForward | FftOperation::C2cInverse => FFT_EXTENSION_FAMILY_ID,
1813        FftOperation::R2cFull | FftOperation::R2cOnesided => "tenferro-fft.rfft.v1",
1814        FftOperation::C2r => "tenferro-fft.irfft.v1",
1815    }
1816}
1817
1818fn output_shape_c2c(
1819    shape: &[usize],
1820    axis: usize,
1821    n: Option<usize>,
1822) -> tenferro_tensor::Result<Vec<usize>> {
1823    let len = transform_len(shape, axis, n)?;
1824    let mut out_shape = shape.to_vec();
1825    out_shape[axis] = len;
1826    Ok(out_shape)
1827}
1828
1829fn output_shape_r2c(
1830    shape: &[usize],
1831    axis: usize,
1832    n: Option<usize>,
1833    onesided: bool,
1834) -> tenferro_tensor::Result<Vec<usize>> {
1835    let len = transform_len(shape, axis, n)?;
1836    let mut out_shape = shape.to_vec();
1837    out_shape[axis] = if onesided { len / 2 + 1 } else { len };
1838    Ok(out_shape)
1839}
1840
1841fn output_shape_c2r(
1842    shape: &[usize],
1843    axis: usize,
1844    n: Option<usize>,
1845) -> tenferro_tensor::Result<Vec<usize>> {
1846    validate_axis("irfft", shape, axis)?;
1847    let input_len = shape[axis];
1848    let len = match n {
1849        Some(len) => len,
1850        None => default_c2r_output_len(input_len)?,
1851    };
1852    if len == 0 {
1853        return Err(tenferro_tensor::Error::invalid_argument(
1854            "irfft",
1855            "output length",
1856            "must be positive",
1857        ));
1858    }
1859    validate_c2r_spectrum_len(input_len, len)?;
1860    let mut out_shape = shape.to_vec();
1861    out_shape[axis] = len;
1862    Ok(out_shape)
1863}
1864
1865fn output_dim_c2r(input_dim: &SymDim, n: Option<usize>) -> tenferro_tensor::Result<SymDim> {
1866    match (input_dim.constant_value(), n) {
1867        (Some(input_len), Some(output_len)) => {
1868            if output_len == 0 {
1869                return Err(tenferro_tensor::Error::invalid_argument(
1870                    "irfft",
1871                    "output length",
1872                    "must be positive",
1873                ));
1874            }
1875            validate_c2r_spectrum_len(input_len, output_len)?;
1876            Ok(SymDim::from(output_len))
1877        }
1878        (Some(input_len), None) => Ok(SymDim::from(default_c2r_output_len(input_len)?)),
1879        (None, Some(output_len)) => {
1880            if output_len == 0 {
1881                return Err(tenferro_tensor::Error::invalid_argument(
1882                    "irfft",
1883                    "output length",
1884                    "must be positive",
1885                ));
1886            }
1887            Ok(SymDim::from(output_len))
1888        }
1889        (None, None) => Ok((input_dim.clone() - 1usize) * 2usize),
1890    }
1891}
1892
1893fn default_c2r_output_len(input_len: usize) -> tenferro_tensor::Result<usize> {
1894    if input_len == 0 {
1895        return Err(tenferro_tensor::Error::invalid_argument(
1896            "irfft",
1897            "input spectrum axis length",
1898            "must be positive",
1899        ));
1900    }
1901    input_len
1902        .checked_sub(1)
1903        .and_then(|len| len.checked_mul(2))
1904        .ok_or_else(|| {
1905            tenferro_tensor::Error::invalid_argument(
1906                "irfft",
1907                "default output length",
1908                "overflows usize",
1909            )
1910        })
1911}
1912
1913fn validate_c2r_spectrum_len(
1914    input_len: usize,
1915    output_len: usize,
1916) -> tenferro_tensor::Result<usize> {
1917    let expected = output_len / 2 + 1;
1918    if input_len != expected {
1919        return Err(tenferro_tensor::Error::invalid_argument(
1920            "irfft",
1921            "spectrum",
1922            format!(
1923                "one-sided spectrum axis length mismatch: expected {expected} for output length {output_len}, got {input_len}"
1924            ),
1925        ));
1926    }
1927    Ok(expected)
1928}
1929
1930fn transform_len(shape: &[usize], axis: usize, n: Option<usize>) -> tenferro_tensor::Result<usize> {
1931    validate_axis("fft", shape, axis)?;
1932    let len = n.unwrap_or(shape[axis]);
1933    if len == 0 {
1934        return Err(tenferro_tensor::Error::invalid_argument(
1935            "fft",
1936            "transform length",
1937            "must be positive",
1938        ));
1939    }
1940    Ok(len)
1941}
1942
1943fn validate_axis(op: &'static str, shape: &[usize], axis: usize) -> tenferro_tensor::Result<()> {
1944    if axis >= shape.len() {
1945        return Err(tenferro_tensor::Error::axis_out_of_bounds(
1946            op,
1947            axis,
1948            shape.len(),
1949        ));
1950    }
1951    Ok(())
1952}
1953
1954#[cfg(test)]
1955mod concrete_tests;
1956
1957#[cfg(test)]
1958mod tests {
1959    use super::*;
1960
1961    #[test]
1962    fn fft_infer_output_meta_rejects_invalid_trait_inputs_without_panicking() {
1963        let op = FftOp::new(FftOperation::R2cOnesided, 0, None, FftNorm::Backward);
1964        let shape = [SymDim::from(4usize)];
1965
1966        assert!(
1967            tenferro_ops::ext_op::invoke_extension_shape_inference(&op, &[], &[&shape]).is_err()
1968        );
1969        assert!(
1970            tenferro_ops::ext_op::invoke_extension_shape_inference(&op, &[DType::F64], &[])
1971                .is_err()
1972        );
1973        assert!(tenferro_ops::ext_op::invoke_extension_shape_inference(
1974            &op,
1975            &[DType::I64],
1976            &[&shape]
1977        )
1978        .is_err());
1979
1980        let bad_axis = FftOp::new(FftOperation::C2cForward, 2, None, FftNorm::Backward);
1981        assert!(tenferro_ops::ext_op::invoke_extension_shape_inference(
1982            &bad_axis,
1983            &[DType::C64],
1984            &[&shape]
1985        )
1986        .is_err());
1987    }
1988
1989    #[test]
1990    fn checked_shape_product_rejects_overflow_before_allocation() {
1991        let err = cpu::checked_shape_product("fft", "output", &[usize::MAX, 2])
1992            .expect_err("overflowing output shape should be rejected");
1993
1994        assert!(err.to_string().contains("overflows usize"), "{err}");
1995    }
1996
1997    #[test]
1998    fn irfft_default_output_length_rejects_overflow() {
1999        let err = output_shape_c2r(&[usize::MAX], 0, None)
2000            .expect_err("default irfft output length should reject overflow");
2001
2002        assert!(err.to_string().contains("overflows usize"), "{err}");
2003    }
2004
2005    #[test]
2006    fn normalize_axis_handles_large_rank_without_isize_cast_wrap() {
2007        assert_eq!(normalize_axis("fft", 0, usize::MAX).unwrap(), 0);
2008        assert_eq!(
2009            normalize_axis("fft", -1, usize::MAX).unwrap(),
2010            usize::MAX - 1
2011        );
2012        assert!(normalize_axis("fft", isize::MIN, 3).is_err());
2013    }
2014
2015    #[test]
2016    fn axis_lane_layout_rejects_stride_overflow() {
2017        let err = cpu::for_axis_lane(&[usize::MAX, 2], 1, 2, |_| Ok(()))
2018            .expect_err("lane layout should reject stride overflow");
2019
2020        assert!(err.to_string().contains("overflows usize"), "{err}");
2021    }
2022
2023    #[cfg(feature = "autodiff")]
2024    #[test]
2025    fn fft_semantic_rules_emit_extension_first_jvp_and_length_restoring_transpose() {
2026        use tenferro_ops::dim_expr::DimExpr;
2027        use tenferro_runtime::program::{ProgramInputSpec, SemanticOpRef, SemanticProgramBuilder};
2028
2029        let fft_op = FftOp::new(FftOperation::C2cForward, 0, Some(2), FftNorm::Backward);
2030        let mut source = SemanticProgramBuilder::new();
2031        let source_input = source
2032            .input(ProgramInputSpec::new(DType::C64, [DimExpr::Const(4)]))
2033            .unwrap();
2034        let source_output = source
2035            .add_extension(Arc::new(fft_op), &[source_input])
2036            .unwrap()[0];
2037        let source = source.finish(&[source_output]).unwrap();
2038        let operation = source.program.operations().next().unwrap();
2039
2040        let rules = semantic_ad_rules().unwrap();
2041        let mut destination = SemanticProgramBuilder::new();
2042        let primal = destination
2043            .input(ProgramInputSpec::new(DType::C64, [DimExpr::Const(4)]))
2044            .unwrap();
2045        let tangent = destination
2046            .input(ProgramInputSpec::new(DType::C64, [DimExpr::Const(4)]))
2047            .unwrap();
2048        let primal_output = destination
2049            .add_extension(
2050                Arc::new(FftOp::new(
2051                    FftOperation::C2cForward,
2052                    0,
2053                    Some(2),
2054                    FftNorm::Backward,
2055                )),
2056                &[primal],
2057            )
2058            .unwrap()[0];
2059        let linearized = rules
2060            .linearize_operation(
2061                operation,
2062                &[primal],
2063                &[primal_output],
2064                &[AdValue::Value(tangent)],
2065                &[true],
2066                &mut destination,
2067            )
2068            .unwrap();
2069        let AdValue::Value(tangent_output) = linearized.tangent_outputs()[0] else {
2070            panic!("FFT tangent must be active");
2071        };
2072        let cotangent_inputs = rules
2073            .linear_transpose_operation(
2074                operation,
2075                &[primal],
2076                &[primal_output],
2077                &[AdValue::Value(tangent_output)],
2078                &[true],
2079                linearized.residuals(),
2080                &mut destination,
2081            )
2082            .unwrap();
2083        let AdValue::Value(cotangent_input) = cotangent_inputs[0] else {
2084            panic!("FFT cotangent must be active");
2085        };
2086        let frozen = destination
2087            .finish(&[tangent_output, cotangent_input])
2088            .unwrap();
2089        let operations: Vec<_> = frozen.program.operations().collect();
2090        assert!(
2091            operations
2092                .iter()
2093                .filter(|operation| matches!(operation.op(), SemanticOpRef::Extension(_)))
2094                .count()
2095                >= 3
2096        );
2097        assert!(operations.iter().any(|operation| matches!(
2098            operation.op(),
2099            SemanticOpRef::Core(CoreSemanticOp::DynamicTruncate { axis: 0 })
2100        )));
2101        assert!(operations.iter().any(|operation| matches!(
2102            operation.op(),
2103            SemanticOpRef::Core(CoreSemanticOp::PadToMatch { axis: 0 })
2104        )));
2105    }
2106
2107    #[cfg(feature = "autodiff")]
2108    #[test]
2109    fn fft_semantic_rules_run_through_whole_program_jvp_and_vjp() {
2110        use tenferro_ad::AdContext;
2111        use tenferro_ops::dim_expr::DimExpr;
2112        use tenferro_runtime::program::{ProgramInputSpec, SemanticOpRef, SemanticProgramBuilder};
2113
2114        let mut builder = SemanticProgramBuilder::new();
2115        let input = builder
2116            .input(ProgramInputSpec::new(DType::C64, [DimExpr::Const(4)]))
2117            .unwrap();
2118        let output = builder
2119            .add_extension(
2120                Arc::new(FftOp::new(
2121                    FftOperation::C2cForward,
2122                    0,
2123                    Some(2),
2124                    FftNorm::Backward,
2125                )),
2126                &[input],
2127            )
2128            .unwrap()[0];
2129        let source = builder.finish(&[output]).unwrap();
2130        let ad = AdContext::builder()
2131            .with_semantic_extension_rules(semantic_ad_rules().unwrap())
2132            .unwrap()
2133            .build()
2134            .unwrap();
2135
2136        let jvp = ad.jvp_program(&source, &[true]).unwrap();
2137        assert_eq!(jvp.derivative_input_indices(), &[Some(1)]);
2138        assert!(matches!(
2139            jvp.frozen().program.operations().last().unwrap().op(),
2140            SemanticOpRef::Extension(op) if op.family_id() == FFT_EXTENSION_FAMILY_ID
2141        ));
2142
2143        let vjp = ad.vjp_program(&source, &[true], &[true]).unwrap();
2144        assert_eq!(vjp.derivative_output_indices(), &[Some(0)]);
2145        assert!(vjp.frozen().program.operations().any(|operation| matches!(
2146            operation.op(),
2147            SemanticOpRef::Core(CoreSemanticOp::PadToMatch { axis: 0 })
2148                | SemanticOpRef::Core(CoreSemanticOp::DynamicTruncate { axis: 0 })
2149        )));
2150    }
2151}