1use std::any::Any;
108use std::hash::Hasher;
109use std::num::NonZeroUsize;
110use std::sync::Arc;
111
112#[cfg(feature = "autodiff")]
113use tenferro_ad::semantic_extension::{
114 AdValue, ResidualSpec, SemanticAdError, SemanticExtensionRegistryError,
115 SemanticExtensionRuleSet, SemanticLinearTransposeRequest, SemanticLinearTransposeRule,
116 SemanticLinearizeRequest, SemanticLinearizeResult, SemanticLinearizeRule,
117 SemanticPrimalVjpRequest, SemanticPrimalVjpRule,
118};
119use tenferro_cpu::with_cpu_exec_session;
120use tenferro_extension_macros::define_extension_runtime;
121#[cfg(feature = "cuda")]
122use tenferro_gpu::cuda::{with_cuda_exec_session, CudaBackend};
123#[cfg(feature = "webgpu")]
124use tenferro_gpu::webgpu::with_webgpu_exec_session;
125use tenferro_ops::SymDim;
126use tenferro_runtime::extension::{
127 apply, ExtensionCacheStore, ExtensionExecutionContext, ExtensionOp,
128};
129#[cfg(feature = "autodiff")]
130use tenferro_runtime::program::{CoreSemanticOp, ProgramValue, SemanticProgramBuilder};
131use tenferro_runtime::{Error, ErrorPhase, Result, TracedTensor};
132use tenferro_tensor::{
133 BackendSession, CacheStats, DType, ErrorKind, Tensor, TensorBackend, TensorRead,
134 ValidationError,
135};
136
137mod backend;
138mod cache;
139mod cpu;
140#[cfg(feature = "cuda")]
141mod cuda;
142#[cfg(feature = "autodiff")]
143mod eager_ext;
144pub mod prelude;
145mod spec;
146#[cfg(feature = "webgpu")]
147mod webgpu;
148
149pub use backend::{FftBackend, FftExecutionCache};
150pub use cache::{
151 fft_plan_cache_selector, FftPlanCache, DEFAULT_FFT_PLAN_CACHE_CAPACITY, FFT_PLAN_CACHE_NAME,
152};
153#[cfg(feature = "autodiff")]
154pub use eager_ext::EagerTensorFftExt;
155pub use spec::{FftNorm, FftOperation, FftPlanSpec};
156
157pub const FFT_EXTENSION_FAMILY_ID: &str = "tenferro-fft.fft.v1";
168
169#[derive(Default)]
176pub struct FftExecutor {
177 plans: FftPlanCache,
178}
179
180impl FftExecutor {
181 pub fn new(plans: FftPlanCache) -> Self {
183 Self { plans }
184 }
185
186 pub const fn plan_cache(&self) -> &FftPlanCache {
188 &self.plans
189 }
190
191 pub fn plan_cache_mut(&mut self) -> &mut FftPlanCache {
193 &mut self.plans
194 }
195
196 pub fn cache_stats(&self) -> CacheStats {
198 self.plans.stats()
199 }
200
201 pub fn clear_cache(&mut self) {
203 self.plans.clear();
204 }
205
206 pub fn fft(
217 &mut self,
218 input: &Tensor,
219 n: Option<usize>,
220 axis: isize,
221 norm: FftNorm,
222 session: &mut dyn BackendSession,
223 ) -> tenferro_tensor::Result<Tensor> {
224 self.execute(
225 input,
226 concrete_fft_operation("FftExecutor::fft", input.dtype())?,
227 "FftExecutor::fft",
228 n,
229 axis,
230 norm,
231 session,
232 )
233 }
234
235 pub fn ifft(
246 &mut self,
247 input: &Tensor,
248 n: Option<usize>,
249 axis: isize,
250 norm: FftNorm,
251 session: &mut dyn BackendSession,
252 ) -> tenferro_tensor::Result<Tensor> {
253 self.execute(
254 input,
255 concrete_ifft_operation("FftExecutor::ifft", input.dtype())?,
256 "FftExecutor::ifft",
257 n,
258 axis,
259 norm,
260 session,
261 )
262 }
263
264 pub fn rfft(
275 &mut self,
276 input: &Tensor,
277 n: Option<usize>,
278 axis: isize,
279 norm: FftNorm,
280 session: &mut dyn BackendSession,
281 ) -> tenferro_tensor::Result<Tensor> {
282 self.execute(
283 input,
284 concrete_rfft_operation("FftExecutor::rfft", input.dtype())?,
285 "FftExecutor::rfft",
286 n,
287 axis,
288 norm,
289 session,
290 )
291 }
292
293 pub fn irfft(
304 &mut self,
305 input: &Tensor,
306 n: Option<usize>,
307 axis: isize,
308 norm: FftNorm,
309 session: &mut dyn BackendSession,
310 ) -> tenferro_tensor::Result<Tensor> {
311 self.execute(
312 input,
313 concrete_irfft_operation("FftExecutor::irfft", input.dtype())?,
314 "FftExecutor::irfft",
315 n,
316 axis,
317 norm,
318 session,
319 )
320 }
321
322 #[allow(clippy::too_many_arguments)]
323 fn execute(
324 &mut self,
325 input: &Tensor,
326 operation: FftOperation,
327 op_name: &'static str,
328 n: Option<usize>,
329 axis: isize,
330 norm: FftNorm,
331 session: &mut dyn BackendSession,
332 ) -> tenferro_tensor::Result<Tensor> {
333 let spec = concrete_fft_spec(
334 op_name,
335 operation,
336 input.dtype(),
337 input.shape(),
338 n,
339 axis,
340 norm,
341 )?;
342 with_fft_exec_session(session, op_name, |backend| {
346 backend.execute_fft(
347 input,
348 &spec,
349 FftExecutionCache::caller_owned(&mut self.plans),
350 )
351 })
352 }
353}
354
355pub trait TracedTensorFftExt {
357 fn fft(&self, n: Option<usize>, axis: isize, norm: FftNorm) -> Result<TracedTensor>;
371
372 fn ifft(&self, n: Option<usize>, axis: isize, norm: FftNorm) -> Result<TracedTensor>;
385
386 fn rfft(&self, n: Option<usize>, axis: isize, norm: FftNorm) -> Result<TracedTensor>;
399
400 fn irfft(&self, n: Option<usize>, axis: isize, norm: FftNorm) -> Result<TracedTensor>;
413}
414
415impl TracedTensorFftExt for TracedTensor {
416 fn fft(&self, n: Option<usize>, axis: isize, norm: FftNorm) -> Result<TracedTensor> {
417 fft(self, n, axis, norm)
418 }
419
420 fn ifft(&self, n: Option<usize>, axis: isize, norm: FftNorm) -> Result<TracedTensor> {
421 ifft(self, n, axis, norm)
422 }
423
424 fn rfft(&self, n: Option<usize>, axis: isize, norm: FftNorm) -> Result<TracedTensor> {
425 rfft(self, n, axis, norm)
426 }
427
428 fn irfft(&self, n: Option<usize>, axis: isize, norm: FftNorm) -> Result<TracedTensor> {
429 irfft(self, n, axis, norm)
430 }
431}
432
433pub trait TensorFftExt {
462 fn fft(
472 &self,
473 n: Option<usize>,
474 axis: isize,
475 norm: FftNorm,
476 session: &mut dyn BackendSession,
477 ) -> tenferro_tensor::Result<Tensor>;
478
479 fn ifft(
489 &self,
490 n: Option<usize>,
491 axis: isize,
492 norm: FftNorm,
493 session: &mut dyn BackendSession,
494 ) -> tenferro_tensor::Result<Tensor>;
495
496 fn rfft(
506 &self,
507 n: Option<usize>,
508 axis: isize,
509 norm: FftNorm,
510 session: &mut dyn BackendSession,
511 ) -> tenferro_tensor::Result<Tensor>;
512
513 fn irfft(
523 &self,
524 n: Option<usize>,
525 axis: isize,
526 norm: FftNorm,
527 session: &mut dyn BackendSession,
528 ) -> tenferro_tensor::Result<Tensor>;
529}
530
531impl TensorFftExt for Tensor {
532 fn fft(
533 &self,
534 n: Option<usize>,
535 axis: isize,
536 norm: FftNorm,
537 session: &mut dyn BackendSession,
538 ) -> tenferro_tensor::Result<Tensor> {
539 let spec = concrete_fft_spec(
540 "TensorFftExt::fft",
541 concrete_fft_operation("TensorFftExt::fft", self.dtype())?,
542 self.dtype(),
543 self.shape(),
544 n,
545 axis,
546 norm,
547 )?;
548 with_fft_exec_session(session, "TensorFftExt::fft", |backend| {
549 execute_concrete_fft_op(self, &spec, backend)
550 })
551 }
552
553 fn ifft(
554 &self,
555 n: Option<usize>,
556 axis: isize,
557 norm: FftNorm,
558 session: &mut dyn BackendSession,
559 ) -> tenferro_tensor::Result<Tensor> {
560 let spec = concrete_fft_spec(
561 "TensorFftExt::ifft",
562 concrete_ifft_operation("TensorFftExt::ifft", self.dtype())?,
563 self.dtype(),
564 self.shape(),
565 n,
566 axis,
567 norm,
568 )?;
569 with_fft_exec_session(session, "TensorFftExt::ifft", |backend| {
570 execute_concrete_fft_op(self, &spec, backend)
571 })
572 }
573
574 fn rfft(
575 &self,
576 n: Option<usize>,
577 axis: isize,
578 norm: FftNorm,
579 session: &mut dyn BackendSession,
580 ) -> tenferro_tensor::Result<Tensor> {
581 let spec = concrete_fft_spec(
582 "TensorFftExt::rfft",
583 concrete_rfft_operation("TensorFftExt::rfft", self.dtype())?,
584 self.dtype(),
585 self.shape(),
586 n,
587 axis,
588 norm,
589 )?;
590 with_fft_exec_session(session, "TensorFftExt::rfft", |backend| {
591 execute_concrete_fft_op(self, &spec, backend)
592 })
593 }
594
595 fn irfft(
596 &self,
597 n: Option<usize>,
598 axis: isize,
599 norm: FftNorm,
600 session: &mut dyn BackendSession,
601 ) -> tenferro_tensor::Result<Tensor> {
602 let spec = concrete_fft_spec(
603 "TensorFftExt::irfft",
604 concrete_irfft_operation("TensorFftExt::irfft", self.dtype())?,
605 self.dtype(),
606 self.shape(),
607 n,
608 axis,
609 norm,
610 )?;
611 with_fft_exec_session(session, "TensorFftExt::irfft", |backend| {
612 execute_concrete_fft_op(self, &spec, backend)
613 })
614 }
615}
616
617pub trait TensorReadFftExt {
645 fn fft_read(
655 &self,
656 n: Option<usize>,
657 axis: isize,
658 norm: FftNorm,
659 session: &mut dyn BackendSession,
660 ) -> tenferro_tensor::Result<Tensor>;
661
662 fn ifft_read(
672 &self,
673 n: Option<usize>,
674 axis: isize,
675 norm: FftNorm,
676 session: &mut dyn BackendSession,
677 ) -> tenferro_tensor::Result<Tensor>;
678
679 fn rfft_read(
689 &self,
690 n: Option<usize>,
691 axis: isize,
692 norm: FftNorm,
693 session: &mut dyn BackendSession,
694 ) -> tenferro_tensor::Result<Tensor>;
695
696 fn irfft_read(
706 &self,
707 n: Option<usize>,
708 axis: isize,
709 norm: FftNorm,
710 session: &mut dyn BackendSession,
711 ) -> tenferro_tensor::Result<Tensor>;
712}
713
714impl TensorReadFftExt for TensorRead<'_> {
715 fn fft_read(
716 &self,
717 n: Option<usize>,
718 axis: isize,
719 norm: FftNorm,
720 session: &mut dyn BackendSession,
721 ) -> tenferro_tensor::Result<Tensor> {
722 with_fft_exec_session(session, "TensorReadFftExt::fft_read", |backend| {
723 execute_concrete_fft_read_op(
724 self,
725 concrete_fft_operation("TensorReadFftExt::fft_read", self.dtype())?,
726 "TensorReadFftExt::fft_read",
727 n,
728 axis,
729 norm,
730 backend,
731 )
732 })
733 }
734
735 fn ifft_read(
736 &self,
737 n: Option<usize>,
738 axis: isize,
739 norm: FftNorm,
740 session: &mut dyn BackendSession,
741 ) -> tenferro_tensor::Result<Tensor> {
742 with_fft_exec_session(session, "TensorReadFftExt::ifft_read", |backend| {
743 execute_concrete_fft_read_op(
744 self,
745 concrete_ifft_operation("TensorReadFftExt::ifft_read", self.dtype())?,
746 "TensorReadFftExt::ifft_read",
747 n,
748 axis,
749 norm,
750 backend,
751 )
752 })
753 }
754
755 fn rfft_read(
756 &self,
757 n: Option<usize>,
758 axis: isize,
759 norm: FftNorm,
760 session: &mut dyn BackendSession,
761 ) -> tenferro_tensor::Result<Tensor> {
762 with_fft_exec_session(session, "TensorReadFftExt::rfft_read", |backend| {
763 execute_concrete_fft_read_op(
764 self,
765 concrete_rfft_operation("TensorReadFftExt::rfft_read", self.dtype())?,
766 "TensorReadFftExt::rfft_read",
767 n,
768 axis,
769 norm,
770 backend,
771 )
772 })
773 }
774
775 fn irfft_read(
776 &self,
777 n: Option<usize>,
778 axis: isize,
779 norm: FftNorm,
780 session: &mut dyn BackendSession,
781 ) -> tenferro_tensor::Result<Tensor> {
782 with_fft_exec_session(session, "TensorReadFftExt::irfft_read", |backend| {
783 execute_concrete_fft_read_op(
784 self,
785 concrete_irfft_operation("TensorReadFftExt::irfft_read", self.dtype())?,
786 "TensorReadFftExt::irfft_read",
787 n,
788 axis,
789 norm,
790 backend,
791 )
792 })
793 }
794}
795
796#[derive(Debug, thiserror::Error)]
797enum FftError {
798 #[error("{op} does not support dtype {dtype:?}; expected {expected}")]
799 UnsupportedDType {
800 op: &'static str,
801 dtype: DType,
802 expected: &'static str,
803 },
804}
805
806#[derive(Clone, Debug, PartialEq)]
807struct FftOp {
808 operation: FftOperation,
809 axis: usize,
810 n: Option<usize>,
811 norm: FftNorm,
812}
813
814impl FftOp {
815 fn new(operation: FftOperation, axis: usize, n: Option<usize>, norm: FftNorm) -> Self {
816 Self {
817 operation,
818 axis,
819 n,
820 norm,
821 }
822 }
823
824 #[cfg(feature = "autodiff")]
825 fn c2c_adjoint(&self) -> Option<Self> {
826 match self.operation {
827 FftOperation::C2cForward => Some(Self {
828 operation: FftOperation::C2cInverse,
829 axis: self.axis,
830 n: self.n,
831 norm: self.norm.c2c_adjoint(),
832 }),
833 FftOperation::C2cInverse => Some(Self {
834 operation: FftOperation::C2cForward,
835 axis: self.axis,
836 n: self.n,
837 norm: self.norm.c2c_adjoint(),
838 }),
839 FftOperation::R2cFull | FftOperation::R2cOnesided | FftOperation::C2r => None,
840 }
841 }
842}
843
844impl ExtensionOp for FftOp {
845 fn family_id(&self) -> &'static str {
846 FFT_EXTENSION_FAMILY_ID
847 }
848
849 fn payload_hash(&self, hasher: &mut dyn Hasher) {
850 let operation = match self.operation {
851 FftOperation::C2cForward => 0,
852 FftOperation::C2cInverse => 1,
853 FftOperation::R2cOnesided => 2,
854 FftOperation::R2cFull => 3,
855 FftOperation::C2r => 4,
856 };
857 hasher.write_u8(operation);
858 hasher.write_usize(self.axis);
859 match self.n {
860 Some(n) => {
861 hasher.write_u8(1);
862 hasher.write_usize(n);
863 }
864 None => hasher.write_u8(0),
865 }
866 let norm = match self.norm {
867 FftNorm::Backward => 0,
868 FftNorm::Forward => 1,
869 FftNorm::Ortho => 2,
870 };
871 hasher.write_u8(norm);
872 }
873
874 fn payload_eq(&self, other: &dyn ExtensionOp) -> bool {
875 other
876 .as_any()
877 .downcast_ref::<FftOp>()
878 .is_some_and(|that| self == that)
879 }
880
881 fn clone_arc(&self) -> Arc<dyn ExtensionOp> {
882 Arc::new(self.clone())
883 }
884
885 fn as_any(&self) -> &dyn Any {
886 self
887 }
888
889 fn input_count(&self) -> usize {
890 1
891 }
892
893 fn output_count(&self) -> usize {
894 1
895 }
896
897 fn semantic_effects(&self) -> tenferro_ops::ext_op::ExtensionEffectDeclaration<'_> {
898 tenferro_ops::ext_op::ExtensionEffectDeclaration::Declared(&[])
899 }
900
901 fn semantic_aliases(&self) -> tenferro_ops::ext_op::ExtensionAliasDeclaration<'_> {
902 tenferro_ops::ext_op::ExtensionAliasDeclaration::AllFresh
903 }
904
905 fn infer_output_meta(
906 &self,
907 ctx: &mut tenferro_ops::ExtensionShapeContext<'_>,
908 ) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
909 let input_dtype = ctx.input_dtype(0)?;
910 let input_shape = ctx.input_shape(0)?;
911 if self.axis >= input_shape.len() {
912 return Err(tenferro_tensor::Error::axis_out_of_bounds(
913 "tenferro-fft",
914 self.axis,
915 input_shape.len(),
916 ));
917 }
918
919 let mut out_shape = input_shape.to_vec();
920 let output_dtype = match self.operation {
921 FftOperation::C2cForward | FftOperation::C2cInverse => {
922 if !matches!(input_dtype, DType::C32 | DType::C64) {
923 return Err(tensor_unsupported_dtype(
924 "tenferro-fft",
925 input_dtype,
926 "C32 or C64",
927 ));
928 }
929 input_dtype
930 }
931 FftOperation::R2cFull | FftOperation::R2cOnesided => {
932 let len = transform_len_dim(self.n, &input_shape[self.axis]);
933 out_shape[self.axis] = if self.operation.is_onesided() {
934 len / 2usize + 1usize
935 } else {
936 len
937 };
938 match input_dtype {
939 DType::F32 => DType::C32,
940 DType::F64 => DType::C64,
941 _ => {
942 return Err(tensor_unsupported_dtype(
943 "tenferro-fft",
944 input_dtype,
945 "F32 or F64",
946 ));
947 }
948 }
949 }
950 FftOperation::C2r => {
951 out_shape[self.axis] = output_dim_c2r(&input_shape[self.axis], self.n)?;
952 match input_dtype {
953 DType::C32 => DType::F32,
954 DType::C64 => DType::F64,
955 _ => {
956 return Err(tensor_unsupported_dtype(
957 "tenferro-fft",
958 input_dtype,
959 "C32 or C64",
960 ));
961 }
962 }
963 }
964 };
965
966 if self.operation.is_c2c() {
967 out_shape[self.axis] = transform_len_dim(self.n, &input_shape[self.axis]);
968 }
969
970 Ok(vec![(output_dtype, out_shape)])
971 }
972}
973
974fn with_fft_exec_session<X>(
983 session: &mut dyn BackendSession,
984 op: &'static str,
985 f: impl FnOnce(&mut dyn FftBackend) -> tenferro_tensor::Result<X>,
986) -> tenferro_tensor::Result<X> {
987 if with_cpu_exec_session(session, |_| ()).is_some() {
992 return with_cpu_exec_session(session, |exec| f(exec as &mut dyn FftBackend))
993 .expect("marker probe matched a CPU execution session");
994 }
995 #[cfg(feature = "cuda")]
996 if with_cuda_exec_session(session, |_| ()).is_some() {
997 return with_cuda_exec_session(session, |exec| f(exec as &mut dyn FftBackend))
998 .expect("marker probe matched a CUDA execution session");
999 }
1000 #[cfg(feature = "webgpu")]
1001 if with_webgpu_exec_session(session, |_| ()).is_some() {
1002 return with_webgpu_exec_session(session, |exec| f(exec as &mut dyn FftBackend))
1003 .expect("marker probe matched a WebGPU execution session");
1004 }
1005 Err(tenferro_tensor::Error::unsupported(
1006 op,
1007 "selected backend session does not expose an FFT execution capability",
1008 ))
1009}
1010
1011fn execute_concrete_fft_op(
1012 input: &Tensor,
1013 spec: &FftPlanSpec,
1014 backend: &mut dyn FftBackend,
1015) -> tenferro_tensor::Result<Tensor> {
1016 let mut plans = FftPlanCache::with_capacity(NonZeroUsize::MIN);
1017 backend.execute_fft(input, spec, FftExecutionCache::caller_owned(&mut plans))
1018}
1019
1020#[allow(clippy::too_many_arguments)]
1021fn execute_concrete_fft_read_op(
1022 input: &TensorRead<'_>,
1023 operation: FftOperation,
1024 op_name: &'static str,
1025 n: Option<usize>,
1026 axis: isize,
1027 norm: FftNorm,
1028 backend: &mut dyn FftBackend,
1029) -> tenferro_tensor::Result<Tensor> {
1030 let spec = concrete_fft_spec(
1031 op_name,
1032 operation,
1033 input.dtype(),
1034 input.shape(),
1035 n,
1036 axis,
1037 norm,
1038 )?;
1039 let materialized = backend.to_contiguous_read(input.clone())?;
1040 let mut plans = FftPlanCache::with_capacity(NonZeroUsize::MIN);
1041 backend.execute_fft(
1042 &materialized,
1043 &spec,
1044 FftExecutionCache::caller_owned(&mut plans),
1045 )
1046}
1047
1048#[allow(clippy::too_many_arguments)]
1049fn concrete_fft_spec(
1050 op: &'static str,
1051 operation: FftOperation,
1052 input_dtype: DType,
1053 input_shape: &[usize],
1054 n: Option<usize>,
1055 axis: isize,
1056 norm: FftNorm,
1057) -> tenferro_tensor::Result<FftPlanSpec> {
1058 validate_concrete_n(op, n)?;
1059 let axis = normalize_concrete_axis(op, axis, input_shape.len())?;
1060 validated_fft_plan_spec(op, operation, input_dtype, input_shape, n, axis, norm)
1061}
1062
1063#[allow(clippy::too_many_arguments)]
1064fn validated_fft_plan_spec(
1065 op: &'static str,
1066 operation: FftOperation,
1067 input_dtype: DType,
1068 input_shape: &[usize],
1069 n: Option<usize>,
1070 axis: usize,
1071 norm: FftNorm,
1072) -> tenferro_tensor::Result<FftPlanSpec> {
1073 validate_concrete_n(op, n)?;
1074 validate_operation_dtype(op, operation, input_dtype)?;
1075 validate_axis(op, input_shape, axis)?;
1076 validate_concrete_transform_len(op, input_shape, n, axis)?;
1077 if operation == FftOperation::C2r {
1078 output_shape_c2r(input_shape, axis, n)?;
1079 }
1080 Ok(FftPlanSpec::new(
1081 operation,
1082 axis,
1083 n,
1084 norm,
1085 input_dtype,
1086 input_shape.to_vec(),
1087 ))
1088}
1089
1090fn concrete_fft_operation(op: &'static str, dtype: DType) -> tenferro_tensor::Result<FftOperation> {
1091 match dtype {
1092 DType::C32 | DType::C64 => Ok(FftOperation::C2cForward),
1093 DType::F32 | DType::F64 => Ok(FftOperation::R2cFull),
1094 DType::I32 | DType::I64 | DType::Bool => {
1095 Err(tensor_unsupported_dtype(op, dtype, "F32, F64, C32, or C64"))
1096 }
1097 }
1098}
1099
1100fn concrete_ifft_operation(
1101 op: &'static str,
1102 dtype: DType,
1103) -> tenferro_tensor::Result<FftOperation> {
1104 match dtype {
1105 DType::C32 | DType::C64 => Ok(FftOperation::C2cInverse),
1106 DType::F32 | DType::F64 | DType::I32 | DType::I64 | DType::Bool => {
1107 Err(tensor_unsupported_dtype(op, dtype, "C32 or C64"))
1108 }
1109 }
1110}
1111
1112fn concrete_rfft_operation(
1113 op: &'static str,
1114 dtype: DType,
1115) -> tenferro_tensor::Result<FftOperation> {
1116 match dtype {
1117 DType::F32 | DType::F64 => Ok(FftOperation::R2cOnesided),
1118 DType::C32 | DType::C64 | DType::I32 | DType::I64 | DType::Bool => {
1119 Err(tensor_unsupported_dtype(op, dtype, "F32 or F64"))
1120 }
1121 }
1122}
1123
1124fn concrete_irfft_operation(
1125 op: &'static str,
1126 dtype: DType,
1127) -> tenferro_tensor::Result<FftOperation> {
1128 match dtype {
1129 DType::C32 | DType::C64 => Ok(FftOperation::C2r),
1130 DType::F32 | DType::F64 | DType::I32 | DType::I64 | DType::Bool => {
1131 Err(tensor_unsupported_dtype(op, dtype, "C32 or C64"))
1132 }
1133 }
1134}
1135
1136fn validate_operation_dtype(
1137 op: &'static str,
1138 operation: FftOperation,
1139 dtype: DType,
1140) -> tenferro_tensor::Result<()> {
1141 let supported = match operation {
1142 FftOperation::C2cForward | FftOperation::C2cInverse | FftOperation::C2r => {
1143 matches!(dtype, DType::C32 | DType::C64)
1144 }
1145 FftOperation::R2cFull | FftOperation::R2cOnesided => {
1146 matches!(dtype, DType::F32 | DType::F64)
1147 }
1148 };
1149 if supported {
1150 Ok(())
1151 } else {
1152 Err(tensor_unsupported_dtype(
1153 op,
1154 dtype,
1155 expected_dtype_description(operation),
1156 ))
1157 }
1158}
1159
1160fn validate_concrete_n(op: &'static str, n: Option<usize>) -> tenferro_tensor::Result<()> {
1161 if n == Some(0) {
1162 return Err(tenferro_tensor::Error::invalid_argument(
1163 op,
1164 "n",
1165 "transform length must be positive",
1166 ));
1167 }
1168 Ok(())
1169}
1170
1171fn validate_concrete_transform_len(
1172 op: &'static str,
1173 input_shape: &[usize],
1174 n: Option<usize>,
1175 axis: usize,
1176) -> tenferro_tensor::Result<()> {
1177 if n.is_none() && input_shape.get(axis).copied() == Some(0) {
1178 return Err(tenferro_tensor::Error::invalid_argument(
1179 op,
1180 "n",
1181 "transform length must be positive",
1182 ));
1183 }
1184 Ok(())
1185}
1186
1187fn normalize_concrete_axis(
1188 op: &'static str,
1189 axis: isize,
1190 rank: usize,
1191) -> tenferro_tensor::Result<usize> {
1192 if rank == 0 {
1193 return Err(tenferro_tensor::Error::invalid_argument(
1194 op,
1195 "rank",
1196 "FFT requires rank >= 1",
1197 ));
1198 }
1199 let normalized = if axis >= 0 {
1200 axis as usize
1201 } else {
1202 rank.checked_sub(axis.unsigned_abs()).ok_or_else(|| {
1203 tenferro_tensor::Error::axis_out_of_bounds(op, axis.unsigned_abs(), rank)
1204 })?
1205 };
1206 if normalized >= rank {
1207 return Err(tenferro_tensor::Error::axis_out_of_bounds(
1208 op, normalized, rank,
1209 ));
1210 }
1211 Ok(normalized)
1212}
1213
1214fn tensor_unsupported_dtype(
1215 op: &'static str,
1216 dtype: DType,
1217 expected: &'static str,
1218) -> tenferro_tensor::Error {
1219 tenferro_tensor::Error::extension(
1220 op,
1221 FFT_EXTENSION_FAMILY_ID,
1222 ErrorKind::Unsupported,
1223 FftError::UnsupportedDType {
1224 op,
1225 dtype,
1226 expected,
1227 },
1228 )
1229}
1230
1231#[cfg(feature = "autodiff")]
1232#[derive(Debug)]
1233struct FftAdRule;
1234
1235#[cfg(feature = "autodiff")]
1236impl SemanticLinearizeRule for FftAdRule {
1237 fn family_id(&self) -> &'static str {
1238 FFT_EXTENSION_FAMILY_ID
1239 }
1240
1241 fn linearize(
1242 &self,
1243 request: SemanticLinearizeRequest<'_>,
1244 builder: &mut SemanticProgramBuilder,
1245 ) -> std::result::Result<SemanticLinearizeResult, SemanticAdError> {
1246 let fft_op = semantic_fft_payload(request.op(), SemanticAdRuleKind::Linearize)?;
1247 if !fft_op.operation.is_c2c() {
1248 return Err(semantic_fft_unsupported(
1249 fft_op.operation,
1250 SemanticAdRuleKind::Linearize,
1251 ));
1252 }
1253 let tangent = match request.tangent_inputs()[0] {
1254 AdValue::Absent => AdValue::Absent,
1255 AdValue::Value(tangent) => {
1256 AdValue::Value(builder.add_extension(Arc::new(fft_op.clone()), &[tangent])?[0])
1257 }
1258 };
1259 Ok(SemanticLinearizeResult::new([tangent], []))
1260 }
1261}
1262
1263#[cfg(feature = "autodiff")]
1264impl SemanticLinearTransposeRule for FftAdRule {
1265 fn family_id(&self) -> &'static str {
1266 FFT_EXTENSION_FAMILY_ID
1267 }
1268
1269 fn residual_mask(&self) -> ResidualSpec {
1270 ResidualSpec::input(0)
1273 }
1274
1275 fn linear_transpose(
1276 &self,
1277 request: SemanticLinearTransposeRequest<'_>,
1278 builder: &mut SemanticProgramBuilder,
1279 ) -> std::result::Result<Box<[AdValue]>, SemanticAdError> {
1280 Ok([semantic_fft_adjoint(
1281 request.op(),
1282 request.cotangent_outputs()[0],
1283 request.active_inputs()[0],
1284 request.primal_input_value(0)?,
1285 request.residual_mask(),
1286 builder,
1287 )?]
1288 .into())
1289 }
1290}
1291
1292#[cfg(feature = "autodiff")]
1293impl SemanticPrimalVjpRule for FftAdRule {
1294 fn family_id(&self) -> &'static str {
1295 FFT_EXTENSION_FAMILY_ID
1296 }
1297
1298 fn residual_mask(&self) -> ResidualSpec {
1299 ResidualSpec::input(0)
1300 }
1301
1302 fn primal_vjp(
1303 &self,
1304 request: SemanticPrimalVjpRequest<'_>,
1305 builder: &mut SemanticProgramBuilder,
1306 ) -> std::result::Result<Box<[AdValue]>, SemanticAdError> {
1307 Ok([semantic_fft_adjoint(
1308 request.op(),
1309 request.cotangent_outputs()[0],
1310 request.active_inputs()[0],
1311 request.primal_input_value(0)?,
1312 request.residual_mask(),
1313 builder,
1314 )?]
1315 .into())
1316 }
1317}
1318
1319#[cfg(feature = "autodiff")]
1320#[derive(Clone, Copy)]
1321enum SemanticAdRuleKind {
1322 Linearize,
1323 Transpose,
1324}
1325
1326#[cfg(feature = "autodiff")]
1327fn semantic_fft_payload(
1328 op: &dyn ExtensionOp,
1329 role: SemanticAdRuleKind,
1330) -> std::result::Result<&FftOp, SemanticAdError> {
1331 op.as_any().downcast_ref::<FftOp>().ok_or_else(|| {
1332 semantic_fft_unsupported_family(
1333 FFT_EXTENSION_FAMILY_ID,
1334 role,
1335 "FFT semantic AD received an incompatible extension payload",
1336 )
1337 })
1338}
1339
1340#[cfg(feature = "autodiff")]
1341fn semantic_fft_adjoint(
1342 op: &dyn ExtensionOp,
1343 cotangent: AdValue,
1344 active: bool,
1345 primal_input: ProgramValue,
1346 residual_mask: ResidualSpec,
1347 builder: &mut SemanticProgramBuilder,
1348) -> std::result::Result<AdValue, SemanticAdError> {
1349 if !active {
1350 return Ok(AdValue::Absent);
1351 }
1352 let AdValue::Value(cotangent) = cotangent else {
1353 return Ok(AdValue::Absent);
1354 };
1355 let fft_op = semantic_fft_payload(op, SemanticAdRuleKind::Transpose)?;
1356 if !fft_op.operation.is_c2c() {
1357 return Err(semantic_fft_unsupported(
1358 fft_op.operation,
1359 SemanticAdRuleKind::Transpose,
1360 ));
1361 }
1362 let adjoint_op = fft_op
1363 .c2c_adjoint()
1364 .ok_or_else(|| semantic_fft_unsupported(fft_op.operation, SemanticAdRuleKind::Transpose))?;
1365 let adjoint = builder.add_extension(Arc::new(adjoint_op), &[cotangent])?[0];
1366 restore_semantic_c2c_adjoint_input_length(builder, adjoint, primal_input, residual_mask, fft_op)
1367 .map(AdValue::Value)
1368}
1369
1370#[cfg(feature = "autodiff")]
1371fn restore_semantic_c2c_adjoint_input_length(
1372 builder: &mut SemanticProgramBuilder,
1373 adjoint: ProgramValue,
1374 primal_input: ProgramValue,
1375 residual_mask: ResidualSpec,
1376 fft_op: &FftOp,
1377) -> std::result::Result<ProgramValue, SemanticAdError> {
1378 let Some(transform_len) = fft_op.n else {
1379 return Ok(adjoint);
1380 };
1381 debug_assert!(
1382 residual_mask.declares_input(0),
1383 "fft transpose read primal input 0 as a tensor operand but the residual mask does not \
1384 declare it; declare it in the fft rule's residual mask"
1385 );
1386 let input_len = builder
1387 .value_metadata(primal_input)?
1388 .shape()
1389 .get(fft_op.axis)
1390 .and_then(|extent| extent.as_exact())
1391 .and_then(|dim| match dim {
1392 tenferro_ops::dim_expr::DimExpr::Const(value) => Some(*value),
1393 _ => None,
1394 });
1395 if input_len == Some(transform_len) {
1396 return Ok(adjoint);
1397 }
1398
1399 let size = builder.add_op(
1400 CoreSemanticOp::ShapeOf { axis: fft_op.axis },
1401 &[primal_input],
1402 )?[0];
1403 let truncated = builder.add_op(
1404 CoreSemanticOp::DynamicTruncate { axis: fft_op.axis },
1405 &[adjoint, size],
1406 )?[0];
1407 Ok(builder.add_op(
1408 CoreSemanticOp::PadToMatch { axis: fft_op.axis },
1409 &[truncated, primal_input],
1410 )?[0])
1411}
1412
1413#[cfg(feature = "autodiff")]
1414fn semantic_fft_unsupported(operation: FftOperation, role: SemanticAdRuleKind) -> SemanticAdError {
1415 semantic_fft_unsupported_family(
1416 fft_ad_family_id(operation),
1417 role,
1418 "FFT operation has no semantic AD rule",
1419 )
1420}
1421
1422#[cfg(feature = "autodiff")]
1423fn semantic_fft_unsupported_family(
1424 family_id: &'static str,
1425 role: SemanticAdRuleKind,
1426 message: impl Into<String>,
1427) -> SemanticAdError {
1428 SemanticAdError::Unsupported {
1429 family_id,
1430 role: match role {
1431 SemanticAdRuleKind::Linearize => {
1432 tenferro_ad::semantic_extension::SemanticAdRuleRole::Linearize
1433 }
1434 SemanticAdRuleKind::Transpose => {
1435 tenferro_ad::semantic_extension::SemanticAdRuleRole::LinearTranspose
1436 }
1437 },
1438 message: message.into(),
1439 }
1440}
1441
1442#[cfg(feature = "autodiff")]
1444pub fn semantic_ad_rules(
1452) -> std::result::Result<SemanticExtensionRuleSet, SemanticExtensionRegistryError> {
1453 SemanticExtensionRuleSet::new()
1454 .with_linearize(Arc::new(FftAdRule))?
1455 .with_linear_transpose(Arc::new(FftAdRule))?
1456 .with_primal_vjp(Arc::new(FftAdRule))
1457}
1458
1459pub(crate) fn execute_fft_extension_reads_owner<B: TensorBackend + 'static>(
1460 op: &FftOp,
1461 inputs: &[TensorRead<'_>],
1462 ctx: &mut ExtensionExecutionContext<'_, B>,
1463) -> tenferro_tensor::Result<Vec<Tensor>> {
1464 let (backend, caches) = ctx.parts_mut();
1465 backend.with_backend_session(|session| {
1466 execute_fft_extension_reads_on_session(op, inputs, session, caches)
1467 })
1468}
1469
1470pub(crate) fn execute_fft_extension_reads_session(
1471 op: &FftOp,
1472 inputs: &[TensorRead<'_>],
1473 ctx: &mut ExtensionExecutionContext<'_, dyn BackendSession + '_>,
1474) -> tenferro_tensor::Result<Vec<Tensor>> {
1475 let (session, caches) = ctx.parts_mut();
1476 execute_fft_extension_reads_on_session(op, inputs, session, caches)
1477}
1478
1479fn execute_fft_extension_for_capability<B: FftBackend + ?Sized>(
1480 op: &FftOp,
1481 inputs: &[&Tensor],
1482 session: &mut B,
1483 caches: &mut ExtensionCacheStore,
1484) -> tenferro_tensor::Result<Vec<Tensor>> {
1485 if inputs.len() != 1 {
1486 return Err(tenferro_tensor::Error::invalid_argument(
1487 "tenferro-fft",
1488 "inputs",
1489 format!("expected 1 input, got {}", inputs.len()),
1490 ));
1491 }
1492 let input = inputs[0];
1493 let spec = validated_fft_plan_spec(
1494 fft_op_name(op.operation),
1495 op.operation,
1496 input.dtype(),
1497 input.shape(),
1498 op.n,
1499 op.axis,
1500 op.norm,
1501 )?;
1502 let output = session.execute_fft(input, &spec, FftExecutionCache::runtime_owned(caches))?;
1503 Ok(vec![output])
1504}
1505
1506fn execute_fft_extension_reads_on_session(
1507 op: &FftOp,
1508 inputs: &[TensorRead<'_>],
1509 session: &mut dyn BackendSession,
1510 caches: &mut ExtensionCacheStore,
1511) -> tenferro_tensor::Result<Vec<Tensor>> {
1512 if let Some(result) = with_cpu_exec_session(session, |session| {
1513 execute_fft_extension_reads_for_capability(op, inputs, session, caches)
1514 }) {
1515 return result;
1516 }
1517 #[cfg(feature = "cuda")]
1518 if let Some(result) = with_cuda_exec_session(session, |session| {
1519 execute_fft_extension_reads_for_capability(op, inputs, session, caches)
1520 }) {
1521 return result;
1522 }
1523 #[cfg(feature = "webgpu")]
1524 if let Some(result) = with_webgpu_exec_session(session, |session| {
1525 execute_fft_extension_reads_for_capability(op, inputs, session, caches)
1526 }) {
1527 return result;
1528 }
1529 Err(tenferro_tensor::Error::unsupported(
1530 fft_op_name(op.operation),
1531 "selected backend session does not expose an FFT execution capability",
1532 ))
1533}
1534
1535fn execute_fft_extension_reads_for_capability<B: FftBackend + ?Sized>(
1536 op: &FftOp,
1537 inputs: &[TensorRead<'_>],
1538 session: &mut B,
1539 caches: &mut ExtensionCacheStore,
1540) -> tenferro_tensor::Result<Vec<Tensor>> {
1541 let op_name = fft_op_name(op.operation);
1542 for input in inputs {
1543 session.validate_fft_read_input(op_name, input)?;
1544 }
1545 let materialized_inputs = inputs
1546 .iter()
1547 .cloned()
1548 .map(|input| session.to_contiguous_read(input))
1549 .collect::<tenferro_tensor::Result<Vec<_>>>()?;
1550 let input_refs: Vec<&Tensor> = materialized_inputs.iter().collect();
1551 execute_fft_extension_for_capability(op, &input_refs, session, caches)
1552}
1553
1554define_extension_runtime! {
1555 runtime = FftRuntime,
1556 family_id = FFT_EXTENSION_FAMILY_ID,
1557 op_type = FftOp,
1558 execute = execute_fft_extension_reads_owner,
1559 execute_reads = execute_fft_extension_reads_owner,
1560 execute_in_session = execute_fft_extension_reads_in_session,
1561 session_supported = fft_session_supported,
1562 backend_bound = TensorBackend,
1563}
1564
1565fn execute_fft_extension_reads_in_session(
1569 op: &FftOp,
1570 session: &mut dyn BackendSession,
1571 caches: &mut ExtensionCacheStore,
1572 inputs: &[TensorRead<'_>],
1573) -> tenferro_tensor::Result<Vec<Tensor>> {
1574 let mut ctx = ExtensionExecutionContext::new(session, caches);
1575 execute_fft_extension_reads_session(op, inputs, &mut ctx)
1576}
1577
1578fn fft_session_supported<B: BackendSession + 'static>(_op: &FftOp) -> bool {
1579 let type_id = std::any::TypeId::of::<B>();
1583 type_id == std::any::TypeId::of::<tenferro_cpu::CpuBackend>() || {
1584 #[cfg(feature = "cuda")]
1585 {
1586 type_id == std::any::TypeId::of::<CudaBackend>()
1587 }
1588 #[cfg(not(feature = "cuda"))]
1589 {
1590 false
1591 }
1592 }
1593}
1594
1595fn fft(input: &TracedTensor, n: Option<usize>, axis: isize, norm: FftNorm) -> Result<TracedTensor> {
1627 let operation = runtime_forward_fft_operation(input.dtype)?;
1628 apply_unary_fft("fft", input, operation, n, axis, norm)
1629}
1630
1631fn ifft(
1660 input: &TracedTensor,
1661 n: Option<usize>,
1662 axis: isize,
1663 norm: FftNorm,
1664) -> Result<TracedTensor> {
1665 require_runtime_dtype("ifft", input.dtype, &[DType::C32, DType::C64], "C32 or C64")?;
1666 apply_unary_fft("ifft", input, FftOperation::C2cInverse, n, axis, norm)
1667}
1668
1669fn rfft(
1702 input: &TracedTensor,
1703 n: Option<usize>,
1704 axis: isize,
1705 norm: FftNorm,
1706) -> Result<TracedTensor> {
1707 require_runtime_dtype("rfft", input.dtype, &[DType::F32, DType::F64], "F32 or F64")?;
1708 apply_unary_fft("rfft", input, FftOperation::R2cOnesided, n, axis, norm)
1709}
1710
1711fn irfft(
1747 input: &TracedTensor,
1748 n: Option<usize>,
1749 axis: isize,
1750 norm: FftNorm,
1751) -> Result<TracedTensor> {
1752 require_runtime_dtype(
1753 "irfft",
1754 input.dtype,
1755 &[DType::C32, DType::C64],
1756 "C32 or C64",
1757 )?;
1758 apply_unary_fft("irfft", input, FftOperation::C2r, n, axis, norm)
1759}
1760
1761fn apply_unary_fft(
1762 op_name: &'static str,
1763 input: &TracedTensor,
1764 operation: FftOperation,
1765 n: Option<usize>,
1766 axis: isize,
1767 norm: FftNorm,
1768) -> Result<TracedTensor> {
1769 let concrete_shape = input.try_concrete_shape();
1770 let op = Arc::new(prepare_runtime_fft_op(
1771 op_name,
1772 operation,
1773 input.rank,
1774 concrete_shape.as_deref(),
1775 n,
1776 axis,
1777 norm,
1778 )?);
1779 let mut outputs = apply(op, &[input])?;
1780 outputs
1781 .pop()
1782 .ok_or_else(|| Error::Internal("FFT extension declares exactly one output".into()))
1783}
1784
1785fn normalize_axis(op: &'static str, axis: isize, rank: usize) -> Result<usize> {
1786 if rank == 0 {
1787 return Err(runtime_invalid_argument(
1788 op,
1789 "rank",
1790 "FFT requires rank >= 1",
1791 ));
1792 }
1793 let normalized = if axis >= 0 {
1794 axis as usize
1795 } else {
1796 rank.checked_sub(axis.unsigned_abs())
1797 .ok_or_else(|| runtime_axis_out_of_bounds(op, axis.unsigned_abs(), rank))?
1798 };
1799 if normalized >= rank {
1800 return Err(runtime_axis_out_of_bounds(op, normalized, rank));
1801 }
1802 Ok(normalized)
1803}
1804
1805fn validate_n(op: &'static str, n: Option<usize>) -> Result<()> {
1806 if n == Some(0) {
1807 return Err(runtime_invalid_argument(
1808 op,
1809 "n",
1810 "transform length must be positive",
1811 ));
1812 }
1813 Ok(())
1814}
1815
1816fn prepare_runtime_fft_op(
1817 op: &'static str,
1818 operation: FftOperation,
1819 rank: usize,
1820 concrete_shape: Option<&[usize]>,
1821 n: Option<usize>,
1822 axis: isize,
1823 norm: FftNorm,
1824) -> Result<FftOp> {
1825 validate_n(op, n)?;
1826 let axis = normalize_axis(op, axis, rank)?;
1827 if n.is_none() && concrete_shape.and_then(|shape| shape.get(axis).copied()) == Some(0) {
1828 return Err(runtime_invalid_argument(
1829 op,
1830 "n",
1831 "transform length must be positive",
1832 ));
1833 }
1834 if operation == FftOperation::C2r {
1835 if let Some(shape) = concrete_shape {
1836 output_shape_c2r(shape, axis, n)?;
1837 }
1838 }
1839 Ok(FftOp::new(operation, axis, n, norm))
1840}
1841
1842fn runtime_forward_fft_operation(dtype: DType) -> Result<FftOperation> {
1843 match dtype {
1844 DType::C32 | DType::C64 => Ok(FftOperation::C2cForward),
1845 DType::F32 | DType::F64 => Ok(FftOperation::R2cFull),
1846 DType::I32 | DType::I64 | DType::Bool => Err(runtime_unsupported_dtype(
1847 "fft",
1848 dtype,
1849 "F32, F64, C32, or C64",
1850 )),
1851 }
1852}
1853
1854fn require_runtime_dtype(
1855 op: &'static str,
1856 dtype: DType,
1857 supported: &[DType],
1858 expected: &'static str,
1859) -> Result<()> {
1860 if supported.contains(&dtype) {
1861 Ok(())
1862 } else {
1863 Err(runtime_unsupported_dtype(op, dtype, expected))
1864 }
1865}
1866
1867fn runtime_invalid_argument(
1868 op: &'static str,
1869 argument: &'static str,
1870 message: impl Into<String>,
1871) -> Error {
1872 Error::validation(
1873 op,
1874 ErrorPhase::GraphBuild,
1875 ValidationError::InvalidArgument {
1876 argument,
1877 message: message.into(),
1878 },
1879 )
1880}
1881
1882fn runtime_axis_out_of_bounds(op: &'static str, axis: usize, rank: usize) -> Error {
1883 Error::validation(
1884 op,
1885 ErrorPhase::GraphBuild,
1886 ValidationError::AxisOutOfBounds { axis, rank },
1887 )
1888}
1889
1890fn runtime_unsupported_dtype(op: &'static str, dtype: DType, expected: &'static str) -> Error {
1891 Error::extension(
1892 op,
1893 ErrorPhase::GraphBuild,
1894 FFT_EXTENSION_FAMILY_ID,
1895 ErrorKind::Unsupported,
1896 FftError::UnsupportedDType {
1897 op,
1898 dtype,
1899 expected,
1900 },
1901 )
1902}
1903
1904fn transform_len_dim(n: Option<usize>, input_dim: &SymDim) -> SymDim {
1905 n.map(SymDim::from).unwrap_or_else(|| input_dim.clone())
1906}
1907
1908fn expected_dtype_description(operation: FftOperation) -> &'static str {
1909 match operation {
1910 FftOperation::C2cForward | FftOperation::C2cInverse | FftOperation::C2r => "C32 or C64",
1911 FftOperation::R2cFull | FftOperation::R2cOnesided => "F32 or F64",
1912 }
1913}
1914
1915fn fft_op_name(operation: FftOperation) -> &'static str {
1916 match operation {
1917 FftOperation::C2cForward => "fft",
1918 FftOperation::C2cInverse => "ifft",
1919 FftOperation::R2cFull | FftOperation::R2cOnesided => "rfft",
1920 FftOperation::C2r => "irfft",
1921 }
1922}
1923
1924#[cfg(feature = "autodiff")]
1925fn fft_ad_family_id(operation: FftOperation) -> &'static str {
1926 match operation {
1927 FftOperation::C2cForward | FftOperation::C2cInverse => FFT_EXTENSION_FAMILY_ID,
1928 FftOperation::R2cFull | FftOperation::R2cOnesided => "tenferro-fft.rfft.v1",
1929 FftOperation::C2r => "tenferro-fft.irfft.v1",
1930 }
1931}
1932
1933fn output_shape_c2c(
1934 shape: &[usize],
1935 axis: usize,
1936 n: Option<usize>,
1937) -> tenferro_tensor::Result<Vec<usize>> {
1938 let len = transform_len(shape, axis, n)?;
1939 let mut out_shape = shape.to_vec();
1940 out_shape[axis] = len;
1941 Ok(out_shape)
1942}
1943
1944fn output_shape_r2c(
1945 shape: &[usize],
1946 axis: usize,
1947 n: Option<usize>,
1948 onesided: bool,
1949) -> tenferro_tensor::Result<Vec<usize>> {
1950 let len = transform_len(shape, axis, n)?;
1951 let mut out_shape = shape.to_vec();
1952 out_shape[axis] = if onesided { len / 2 + 1 } else { len };
1953 Ok(out_shape)
1954}
1955
1956fn output_shape_c2r(
1957 shape: &[usize],
1958 axis: usize,
1959 n: Option<usize>,
1960) -> tenferro_tensor::Result<Vec<usize>> {
1961 validate_axis("irfft", shape, axis)?;
1962 let input_len = shape[axis];
1963 let len = match n {
1964 Some(len) => len,
1965 None => default_c2r_output_len(input_len)?,
1966 };
1967 if len == 0 {
1968 return Err(tenferro_tensor::Error::invalid_argument(
1969 "irfft",
1970 "output length",
1971 "must be positive",
1972 ));
1973 }
1974 validate_c2r_spectrum_len(input_len, len)?;
1975 let mut out_shape = shape.to_vec();
1976 out_shape[axis] = len;
1977 Ok(out_shape)
1978}
1979
1980fn output_dim_c2r(input_dim: &SymDim, n: Option<usize>) -> tenferro_tensor::Result<SymDim> {
1981 match (input_dim.constant_value(), n) {
1982 (Some(input_len), Some(output_len)) => {
1983 if output_len == 0 {
1984 return Err(tenferro_tensor::Error::invalid_argument(
1985 "irfft",
1986 "output length",
1987 "must be positive",
1988 ));
1989 }
1990 validate_c2r_spectrum_len(input_len, output_len)?;
1991 Ok(SymDim::from(output_len))
1992 }
1993 (Some(input_len), None) => Ok(SymDim::from(default_c2r_output_len(input_len)?)),
1994 (None, Some(output_len)) => {
1995 if output_len == 0 {
1996 return Err(tenferro_tensor::Error::invalid_argument(
1997 "irfft",
1998 "output length",
1999 "must be positive",
2000 ));
2001 }
2002 Ok(SymDim::from(output_len))
2003 }
2004 (None, None) => Ok((input_dim.clone() - 1usize) * 2usize),
2005 }
2006}
2007
2008fn default_c2r_output_len(input_len: usize) -> tenferro_tensor::Result<usize> {
2009 if input_len == 0 {
2010 return Err(tenferro_tensor::Error::invalid_argument(
2011 "irfft",
2012 "input spectrum axis length",
2013 "must be positive",
2014 ));
2015 }
2016 input_len
2017 .checked_sub(1)
2018 .and_then(|len| len.checked_mul(2))
2019 .ok_or_else(|| {
2020 tenferro_tensor::Error::invalid_argument(
2021 "irfft",
2022 "default output length",
2023 "overflows usize",
2024 )
2025 })
2026}
2027
2028fn validate_c2r_spectrum_len(
2029 input_len: usize,
2030 output_len: usize,
2031) -> tenferro_tensor::Result<usize> {
2032 let expected = output_len / 2 + 1;
2033 if input_len != expected {
2034 return Err(tenferro_tensor::Error::invalid_argument(
2035 "irfft",
2036 "spectrum",
2037 format!(
2038 "one-sided spectrum axis length mismatch: expected {expected} for output length {output_len}, got {input_len}"
2039 ),
2040 ));
2041 }
2042 Ok(expected)
2043}
2044
2045fn transform_len(shape: &[usize], axis: usize, n: Option<usize>) -> tenferro_tensor::Result<usize> {
2046 validate_axis("fft", shape, axis)?;
2047 let len = n.unwrap_or(shape[axis]);
2048 if len == 0 {
2049 return Err(tenferro_tensor::Error::invalid_argument(
2050 "fft",
2051 "transform length",
2052 "must be positive",
2053 ));
2054 }
2055 Ok(len)
2056}
2057
2058fn validate_axis(op: &'static str, shape: &[usize], axis: usize) -> tenferro_tensor::Result<()> {
2059 if axis >= shape.len() {
2060 return Err(tenferro_tensor::Error::axis_out_of_bounds(
2061 op,
2062 axis,
2063 shape.len(),
2064 ));
2065 }
2066 Ok(())
2067}
2068
2069#[cfg(test)]
2070mod concrete_tests;
2071
2072#[cfg(test)]
2073mod tests {
2074 use super::*;
2075
2076 #[test]
2077 fn fft_infer_output_meta_rejects_invalid_trait_inputs_without_panicking() {
2078 let op = FftOp::new(FftOperation::R2cOnesided, 0, None, FftNorm::Backward);
2079 let shape = [SymDim::from(4usize)];
2080
2081 assert!(
2082 tenferro_ops::ext_op::invoke_extension_shape_inference(&op, &[], &[&shape]).is_err()
2083 );
2084 assert!(
2085 tenferro_ops::ext_op::invoke_extension_shape_inference(&op, &[DType::F64], &[])
2086 .is_err()
2087 );
2088 assert!(tenferro_ops::ext_op::invoke_extension_shape_inference(
2089 &op,
2090 &[DType::I64],
2091 &[&shape]
2092 )
2093 .is_err());
2094
2095 let bad_axis = FftOp::new(FftOperation::C2cForward, 2, None, FftNorm::Backward);
2096 assert!(tenferro_ops::ext_op::invoke_extension_shape_inference(
2097 &bad_axis,
2098 &[DType::C64],
2099 &[&shape]
2100 )
2101 .is_err());
2102 }
2103
2104 #[test]
2105 fn checked_shape_product_rejects_overflow_before_allocation() {
2106 let err = cpu::checked_shape_product("fft", "output", &[usize::MAX, 2])
2107 .expect_err("overflowing output shape should be rejected");
2108
2109 assert!(err.to_string().contains("overflows usize"), "{err}");
2110 }
2111
2112 #[test]
2113 fn irfft_default_output_length_rejects_overflow() {
2114 let err = output_shape_c2r(&[usize::MAX], 0, None)
2115 .expect_err("default irfft output length should reject overflow");
2116
2117 assert!(err.to_string().contains("overflows usize"), "{err}");
2118 }
2119
2120 #[test]
2121 fn normalize_axis_handles_large_rank_without_isize_cast_wrap() {
2122 assert_eq!(normalize_axis("fft", 0, usize::MAX).unwrap(), 0);
2123 assert_eq!(
2124 normalize_axis("fft", -1, usize::MAX).unwrap(),
2125 usize::MAX - 1
2126 );
2127 assert!(normalize_axis("fft", isize::MIN, 3).is_err());
2128 }
2129
2130 #[test]
2131 fn axis_lane_layout_rejects_stride_overflow() {
2132 let err = cpu::for_axis_lane(&[usize::MAX, 2], 1, 2, |_| Ok(()))
2133 .expect_err("lane layout should reject stride overflow");
2134
2135 assert!(err.to_string().contains("overflows usize"), "{err}");
2136 }
2137
2138 #[cfg(feature = "autodiff")]
2139 #[test]
2140 fn fft_semantic_rules_emit_extension_first_jvp_and_length_restoring_transpose() {
2141 use tenferro_ops::dim_expr::DimExpr;
2142 use tenferro_runtime::program::{ProgramInputSpec, SemanticOpRef, SemanticProgramBuilder};
2143
2144 let fft_op = FftOp::new(FftOperation::C2cForward, 0, Some(2), FftNorm::Backward);
2145 let mut source = SemanticProgramBuilder::new();
2146 let source_input = source
2147 .input(ProgramInputSpec::new(DType::C64, [DimExpr::Const(4)]))
2148 .unwrap();
2149 let source_output = source
2150 .add_extension(Arc::new(fft_op), &[source_input])
2151 .unwrap()[0];
2152 let source = source.finish(&[source_output]).unwrap();
2153 let operation = source.program.operations().next().unwrap();
2154
2155 let rules = semantic_ad_rules().unwrap();
2156 let mut destination = SemanticProgramBuilder::new();
2157 let primal = destination
2158 .input(ProgramInputSpec::new(DType::C64, [DimExpr::Const(4)]))
2159 .unwrap();
2160 let tangent = destination
2161 .input(ProgramInputSpec::new(DType::C64, [DimExpr::Const(4)]))
2162 .unwrap();
2163 let primal_output = destination
2164 .add_extension(
2165 Arc::new(FftOp::new(
2166 FftOperation::C2cForward,
2167 0,
2168 Some(2),
2169 FftNorm::Backward,
2170 )),
2171 &[primal],
2172 )
2173 .unwrap()[0];
2174 let linearized = rules
2175 .linearize_operation(
2176 operation,
2177 &[primal],
2178 &[primal_output],
2179 &[AdValue::Value(tangent)],
2180 &[true],
2181 &mut destination,
2182 )
2183 .unwrap();
2184 let AdValue::Value(tangent_output) = linearized.tangent_outputs()[0] else {
2185 panic!("FFT tangent must be active");
2186 };
2187 let cotangent_inputs = rules
2188 .linear_transpose_operation(
2189 operation,
2190 &[primal],
2191 &[primal_output],
2192 &[AdValue::Value(tangent_output)],
2193 &[true],
2194 linearized.residuals(),
2195 &mut destination,
2196 )
2197 .unwrap();
2198 let AdValue::Value(cotangent_input) = cotangent_inputs[0] else {
2199 panic!("FFT cotangent must be active");
2200 };
2201 let frozen = destination
2202 .finish(&[tangent_output, cotangent_input])
2203 .unwrap();
2204 let operations: Vec<_> = frozen.program.operations().collect();
2205 assert!(
2206 operations
2207 .iter()
2208 .filter(|operation| matches!(operation.op(), SemanticOpRef::Extension(_)))
2209 .count()
2210 >= 3
2211 );
2212 assert!(operations.iter().any(|operation| matches!(
2213 operation.op(),
2214 SemanticOpRef::Core(CoreSemanticOp::DynamicTruncate { axis: 0 })
2215 )));
2216 assert!(operations.iter().any(|operation| matches!(
2217 operation.op(),
2218 SemanticOpRef::Core(CoreSemanticOp::PadToMatch { axis: 0 })
2219 )));
2220 }
2221
2222 #[cfg(feature = "autodiff")]
2223 #[test]
2224 fn fft_semantic_rules_run_through_whole_program_jvp_and_vjp() {
2225 use tenferro_ad::AdContext;
2226 use tenferro_ops::dim_expr::DimExpr;
2227 use tenferro_runtime::program::{ProgramInputSpec, SemanticOpRef, SemanticProgramBuilder};
2228
2229 let mut builder = SemanticProgramBuilder::new();
2230 let input = builder
2231 .input(ProgramInputSpec::new(DType::C64, [DimExpr::Const(4)]))
2232 .unwrap();
2233 let output = builder
2234 .add_extension(
2235 Arc::new(FftOp::new(
2236 FftOperation::C2cForward,
2237 0,
2238 Some(2),
2239 FftNorm::Backward,
2240 )),
2241 &[input],
2242 )
2243 .unwrap()[0];
2244 let source = builder.finish(&[output]).unwrap();
2245 let ad = AdContext::builder()
2246 .with_semantic_extension_rules(semantic_ad_rules().unwrap())
2247 .unwrap()
2248 .build()
2249 .unwrap();
2250
2251 let jvp = ad.jvp_program(&source, &[true]).unwrap();
2252 assert_eq!(jvp.derivative_input_indices(), &[Some(1)]);
2253 assert!(matches!(
2254 jvp.frozen().program.operations().last().unwrap().op(),
2255 SemanticOpRef::Extension(op) if op.family_id() == FFT_EXTENSION_FAMILY_ID
2256 ));
2257
2258 let vjp = ad.vjp_program(&source, &[true], &[true]).unwrap();
2259 assert_eq!(vjp.derivative_output_indices(), &[Some(0)]);
2260 assert!(vjp.frozen().program.operations().any(|operation| matches!(
2261 operation.op(),
2262 SemanticOpRef::Core(CoreSemanticOp::PadToMatch { axis: 0 })
2263 | SemanticOpRef::Core(CoreSemanticOp::DynamicTruncate { axis: 0 })
2264 )));
2265 }
2266}