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