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;
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
161pub const FFT_EXTENSION_FAMILY_ID: &str = "tenferro-fft.fft.v1";
172
173#[derive(Default)]
180pub struct FftExecutor {
181 plans: FftPlanCache,
182}
183
184impl FftExecutor {
185 pub fn new(plans: FftPlanCache) -> Self {
187 Self { plans }
188 }
189
190 pub const fn plan_cache(&self) -> &FftPlanCache {
192 &self.plans
193 }
194
195 pub fn plan_cache_mut(&mut self) -> &mut FftPlanCache {
197 &mut self.plans
198 }
199
200 pub fn cache_stats(&self) -> CacheStats {
202 self.plans.stats()
203 }
204
205 pub fn clear_cache(&mut self) {
207 self.plans.clear();
208 }
209
210 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 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 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 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 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
359pub trait TracedTensorFftExt {
361 fn fft(&self, n: Option<usize>, axis: isize, norm: FftNorm) -> Result<TracedTensor>;
375
376 fn ifft(&self, n: Option<usize>, axis: isize, norm: FftNorm) -> Result<TracedTensor>;
389
390 fn rfft(&self, n: Option<usize>, axis: isize, norm: FftNorm) -> Result<TracedTensor>;
403
404 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
437pub trait TensorFftExt {
466 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 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 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 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
621pub trait TensorReadFftExt {
649 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 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 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 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
978fn 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 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 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#[cfg(feature = "autodiff")]
1447pub 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
1554fn 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 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
1584fn 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
1620fn 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
1658fn 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
1700fn 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}