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