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