1use std::any::Any;
125use std::hash::Hasher;
126use std::num::NonZeroUsize;
127use std::sync::Arc;
128
129#[cfg(feature = "autodiff")]
130use tenferro_ad::semantic_extension::{
131 AdValue, SemanticAdError, SemanticExtensionRegistryError, SemanticExtensionRuleSet,
132 SemanticLinearTransposeRequest, SemanticLinearTransposeRule, SemanticLinearizeRequest,
133 SemanticLinearizeResult, SemanticLinearizeRule, SemanticPrimalVjpRequest,
134 SemanticPrimalVjpRule,
135};
136use tenferro_cpu::with_cpu_exec_session;
137use tenferro_extension_macros::define_extension_runtime;
138#[cfg(feature = "webgpu")]
139use tenferro_gpu::with_webgpu_exec_session;
140use tenferro_ops::SymDim;
141use tenferro_runtime::extension::{
142 apply, ExtensionCacheStore, ExtensionExecutionContext, ExtensionOp,
143};
144#[cfg(feature = "autodiff")]
145use tenferro_runtime::program::{CoreSemanticOp, ProgramValue, SemanticProgramBuilder};
146use tenferro_runtime::{Error, ErrorPhase, Result, TracedTensor};
147use tenferro_tensor::{
148 BackendSession, CacheStats, DType, ErrorKind, Tensor, TensorBackend, TensorRead,
149 ValidationError,
150};
151
152mod backend;
153mod cache;
154mod cpu;
155#[cfg(feature = "autodiff")]
156mod eager_ext;
157mod spec;
158#[cfg(feature = "webgpu")]
159mod webgpu;
160
161pub use backend::{FftBackend, FftExecutionCache};
162pub use cache::{
163 fft_plan_cache_selector, FftPlanCache, DEFAULT_FFT_PLAN_CACHE_CAPACITY, FFT_PLAN_CACHE_NAME,
164};
165#[cfg(feature = "autodiff")]
166pub use eager_ext::EagerTensorFftExt;
167pub use spec::{FftNorm, FftOperation, FftPlanSpec};
168
169pub const FFT_EXTENSION_FAMILY_ID: &str = "tenferro-fft.fft.v1";
180
181#[derive(Default)]
188pub struct FftExecutor {
189 plans: FftPlanCache,
190}
191
192impl FftExecutor {
193 pub fn new(plans: FftPlanCache) -> Self {
195 Self { plans }
196 }
197
198 pub const fn plan_cache(&self) -> &FftPlanCache {
200 &self.plans
201 }
202
203 pub fn plan_cache_mut(&mut self) -> &mut FftPlanCache {
205 &mut self.plans
206 }
207
208 pub fn cache_stats(&self) -> CacheStats {
210 self.plans.stats()
211 }
212
213 pub fn clear_cache(&mut self) {
215 self.plans.clear();
216 }
217
218 pub fn fft<B: FftBackend>(
227 &mut self,
228 input: &Tensor,
229 n: Option<usize>,
230 axis: isize,
231 norm: FftNorm,
232 backend: &mut B,
233 ) -> tenferro_tensor::Result<Tensor> {
234 self.execute(
235 input,
236 concrete_fft_operation("FftExecutor::fft", input.dtype())?,
237 "FftExecutor::fft",
238 n,
239 axis,
240 norm,
241 backend,
242 )
243 }
244
245 pub fn ifft<B: FftBackend>(
254 &mut self,
255 input: &Tensor,
256 n: Option<usize>,
257 axis: isize,
258 norm: FftNorm,
259 backend: &mut B,
260 ) -> tenferro_tensor::Result<Tensor> {
261 self.execute(
262 input,
263 concrete_ifft_operation("FftExecutor::ifft", input.dtype())?,
264 "FftExecutor::ifft",
265 n,
266 axis,
267 norm,
268 backend,
269 )
270 }
271
272 pub fn rfft<B: FftBackend>(
281 &mut self,
282 input: &Tensor,
283 n: Option<usize>,
284 axis: isize,
285 norm: FftNorm,
286 backend: &mut B,
287 ) -> tenferro_tensor::Result<Tensor> {
288 self.execute(
289 input,
290 concrete_rfft_operation("FftExecutor::rfft", input.dtype())?,
291 "FftExecutor::rfft",
292 n,
293 axis,
294 norm,
295 backend,
296 )
297 }
298
299 pub fn irfft<B: FftBackend>(
308 &mut self,
309 input: &Tensor,
310 n: Option<usize>,
311 axis: isize,
312 norm: FftNorm,
313 backend: &mut B,
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 backend,
323 )
324 }
325
326 #[allow(clippy::too_many_arguments)]
327 fn execute<B: FftBackend>(
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 backend: &mut B,
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 backend.execute_fft(
347 input,
348 &spec,
349 FftExecutionCache::caller_owned(&mut self.plans),
350 )
351 }
352}
353
354pub trait TracedTensorFftExt {
356 fn fft(&self, n: Option<usize>, axis: isize, norm: FftNorm) -> Result<TracedTensor>;
370
371 fn ifft(&self, n: Option<usize>, axis: isize, norm: FftNorm) -> Result<TracedTensor>;
384
385 fn rfft(&self, n: Option<usize>, axis: isize, norm: FftNorm) -> Result<TracedTensor>;
398
399 fn irfft(&self, n: Option<usize>, axis: isize, norm: FftNorm) -> Result<TracedTensor>;
412}
413
414impl TracedTensorFftExt for TracedTensor {
415 fn fft(&self, n: Option<usize>, axis: isize, norm: FftNorm) -> Result<TracedTensor> {
416 fft(self, n, axis, norm)
417 }
418
419 fn ifft(&self, n: Option<usize>, axis: isize, norm: FftNorm) -> Result<TracedTensor> {
420 ifft(self, n, axis, norm)
421 }
422
423 fn rfft(&self, n: Option<usize>, axis: isize, norm: FftNorm) -> Result<TracedTensor> {
424 rfft(self, n, axis, norm)
425 }
426
427 fn irfft(&self, n: Option<usize>, axis: isize, norm: FftNorm) -> Result<TracedTensor> {
428 irfft(self, n, axis, norm)
429 }
430}
431
432pub trait TensorFftExt {
465 fn fft<B: FftBackend>(
473 &self,
474 n: Option<usize>,
475 axis: isize,
476 norm: FftNorm,
477 backend: &mut B,
478 ) -> tenferro_tensor::Result<Tensor>;
479
480 fn ifft<B: FftBackend>(
488 &self,
489 n: Option<usize>,
490 axis: isize,
491 norm: FftNorm,
492 backend: &mut B,
493 ) -> tenferro_tensor::Result<Tensor>;
494
495 fn rfft<B: FftBackend>(
503 &self,
504 n: Option<usize>,
505 axis: isize,
506 norm: FftNorm,
507 backend: &mut B,
508 ) -> tenferro_tensor::Result<Tensor>;
509
510 fn irfft<B: FftBackend>(
519 &self,
520 n: Option<usize>,
521 axis: isize,
522 norm: FftNorm,
523 backend: &mut B,
524 ) -> tenferro_tensor::Result<Tensor>;
525}
526
527impl TensorFftExt for Tensor {
528 fn fft<B: FftBackend>(
529 &self,
530 n: Option<usize>,
531 axis: isize,
532 norm: FftNorm,
533 backend: &mut B,
534 ) -> tenferro_tensor::Result<Tensor> {
535 let spec = concrete_fft_spec(
536 "TensorFftExt::fft",
537 concrete_fft_operation("TensorFftExt::fft", self.dtype())?,
538 self.dtype(),
539 self.shape(),
540 n,
541 axis,
542 norm,
543 )?;
544 execute_concrete_fft_op(self, &spec, backend)
545 }
546
547 fn ifft<B: FftBackend>(
548 &self,
549 n: Option<usize>,
550 axis: isize,
551 norm: FftNorm,
552 backend: &mut B,
553 ) -> tenferro_tensor::Result<Tensor> {
554 let spec = concrete_fft_spec(
555 "TensorFftExt::ifft",
556 concrete_ifft_operation("TensorFftExt::ifft", self.dtype())?,
557 self.dtype(),
558 self.shape(),
559 n,
560 axis,
561 norm,
562 )?;
563 execute_concrete_fft_op(self, &spec, backend)
564 }
565
566 fn rfft<B: FftBackend>(
567 &self,
568 n: Option<usize>,
569 axis: isize,
570 norm: FftNorm,
571 backend: &mut B,
572 ) -> tenferro_tensor::Result<Tensor> {
573 let spec = concrete_fft_spec(
574 "TensorFftExt::rfft",
575 concrete_rfft_operation("TensorFftExt::rfft", self.dtype())?,
576 self.dtype(),
577 self.shape(),
578 n,
579 axis,
580 norm,
581 )?;
582 execute_concrete_fft_op(self, &spec, backend)
583 }
584
585 fn irfft<B: FftBackend>(
586 &self,
587 n: Option<usize>,
588 axis: isize,
589 norm: FftNorm,
590 backend: &mut B,
591 ) -> tenferro_tensor::Result<Tensor> {
592 let spec = concrete_fft_spec(
593 "TensorFftExt::irfft",
594 concrete_irfft_operation("TensorFftExt::irfft", self.dtype())?,
595 self.dtype(),
596 self.shape(),
597 n,
598 axis,
599 norm,
600 )?;
601 execute_concrete_fft_op(self, &spec, backend)
602 }
603}
604
605pub trait TensorReadFftExt {
637 fn fft_read<B: FftBackend>(
646 &self,
647 n: Option<usize>,
648 axis: isize,
649 norm: FftNorm,
650 backend: &mut B,
651 ) -> tenferro_tensor::Result<Tensor>;
652
653 fn ifft_read<B: FftBackend>(
661 &self,
662 n: Option<usize>,
663 axis: isize,
664 norm: FftNorm,
665 backend: &mut B,
666 ) -> tenferro_tensor::Result<Tensor>;
667
668 fn rfft_read<B: FftBackend>(
676 &self,
677 n: Option<usize>,
678 axis: isize,
679 norm: FftNorm,
680 backend: &mut B,
681 ) -> tenferro_tensor::Result<Tensor>;
682
683 fn irfft_read<B: FftBackend>(
692 &self,
693 n: Option<usize>,
694 axis: isize,
695 norm: FftNorm,
696 backend: &mut B,
697 ) -> tenferro_tensor::Result<Tensor>;
698}
699
700impl TensorReadFftExt for TensorRead<'_> {
701 fn fft_read<B: FftBackend>(
702 &self,
703 n: Option<usize>,
704 axis: isize,
705 norm: FftNorm,
706 backend: &mut B,
707 ) -> tenferro_tensor::Result<Tensor> {
708 execute_concrete_fft_read_op(
709 self,
710 concrete_fft_operation("TensorReadFftExt::fft_read", self.dtype())?,
711 "TensorReadFftExt::fft_read",
712 n,
713 axis,
714 norm,
715 backend,
716 )
717 }
718
719 fn ifft_read<B: FftBackend>(
720 &self,
721 n: Option<usize>,
722 axis: isize,
723 norm: FftNorm,
724 backend: &mut B,
725 ) -> tenferro_tensor::Result<Tensor> {
726 execute_concrete_fft_read_op(
727 self,
728 concrete_ifft_operation("TensorReadFftExt::ifft_read", self.dtype())?,
729 "TensorReadFftExt::ifft_read",
730 n,
731 axis,
732 norm,
733 backend,
734 )
735 }
736
737 fn rfft_read<B: FftBackend>(
738 &self,
739 n: Option<usize>,
740 axis: isize,
741 norm: FftNorm,
742 backend: &mut B,
743 ) -> tenferro_tensor::Result<Tensor> {
744 execute_concrete_fft_read_op(
745 self,
746 concrete_rfft_operation("TensorReadFftExt::rfft_read", self.dtype())?,
747 "TensorReadFftExt::rfft_read",
748 n,
749 axis,
750 norm,
751 backend,
752 )
753 }
754
755 fn irfft_read<B: FftBackend>(
756 &self,
757 n: Option<usize>,
758 axis: isize,
759 norm: FftNorm,
760 backend: &mut B,
761 ) -> tenferro_tensor::Result<Tensor> {
762 execute_concrete_fft_read_op(
763 self,
764 concrete_irfft_operation("TensorReadFftExt::irfft_read", self.dtype())?,
765 "TensorReadFftExt::irfft_read",
766 n,
767 axis,
768 norm,
769 backend,
770 )
771 }
772}
773
774#[derive(Debug, thiserror::Error)]
775enum FftError {
776 #[error("{op} does not support dtype {dtype:?}; expected {expected}")]
777 UnsupportedDType {
778 op: &'static str,
779 dtype: DType,
780 expected: &'static str,
781 },
782}
783
784#[derive(Clone, Debug, PartialEq)]
785struct FftOp {
786 operation: FftOperation,
787 axis: usize,
788 n: Option<usize>,
789 norm: FftNorm,
790}
791
792impl FftOp {
793 fn new(operation: FftOperation, axis: usize, n: Option<usize>, norm: FftNorm) -> Self {
794 Self {
795 operation,
796 axis,
797 n,
798 norm,
799 }
800 }
801
802 #[cfg(feature = "autodiff")]
803 fn c2c_adjoint(&self) -> Option<Self> {
804 match self.operation {
805 FftOperation::C2cForward => Some(Self {
806 operation: FftOperation::C2cInverse,
807 axis: self.axis,
808 n: self.n,
809 norm: self.norm.c2c_adjoint(),
810 }),
811 FftOperation::C2cInverse => Some(Self {
812 operation: FftOperation::C2cForward,
813 axis: self.axis,
814 n: self.n,
815 norm: self.norm.c2c_adjoint(),
816 }),
817 FftOperation::R2cFull | FftOperation::R2cOnesided | FftOperation::C2r => None,
818 }
819 }
820}
821
822impl ExtensionOp for FftOp {
823 fn family_id(&self) -> &'static str {
824 FFT_EXTENSION_FAMILY_ID
825 }
826
827 fn payload_hash(&self, hasher: &mut dyn Hasher) {
828 let operation = match self.operation {
829 FftOperation::C2cForward => 0,
830 FftOperation::C2cInverse => 1,
831 FftOperation::R2cOnesided => 2,
832 FftOperation::R2cFull => 3,
833 FftOperation::C2r => 4,
834 };
835 hasher.write_u8(operation);
836 hasher.write_usize(self.axis);
837 match self.n {
838 Some(n) => {
839 hasher.write_u8(1);
840 hasher.write_usize(n);
841 }
842 None => hasher.write_u8(0),
843 }
844 let norm = match self.norm {
845 FftNorm::Backward => 0,
846 FftNorm::Forward => 1,
847 FftNorm::Ortho => 2,
848 };
849 hasher.write_u8(norm);
850 }
851
852 fn payload_eq(&self, other: &dyn ExtensionOp) -> bool {
853 other
854 .as_any()
855 .downcast_ref::<FftOp>()
856 .is_some_and(|that| self == that)
857 }
858
859 fn clone_arc(&self) -> Arc<dyn ExtensionOp> {
860 Arc::new(self.clone())
861 }
862
863 fn as_any(&self) -> &dyn Any {
864 self
865 }
866
867 fn input_count(&self) -> usize {
868 1
869 }
870
871 fn output_count(&self) -> usize {
872 1
873 }
874
875 fn semantic_effects(&self) -> tenferro_ops::ext_op::ExtensionEffectDeclaration<'_> {
876 tenferro_ops::ext_op::ExtensionEffectDeclaration::Declared(&[])
877 }
878
879 fn semantic_aliases(&self) -> tenferro_ops::ext_op::ExtensionAliasDeclaration<'_> {
880 tenferro_ops::ext_op::ExtensionAliasDeclaration::AllFresh
881 }
882
883 fn infer_output_meta(
884 &self,
885 ctx: &mut tenferro_ops::ExtensionShapeContext<'_>,
886 ) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
887 let input_dtype = ctx.input_dtype(0)?;
888 let input_shape = ctx.input_shape(0)?;
889 if self.axis >= input_shape.len() {
890 return Err(tenferro_tensor::Error::axis_out_of_bounds(
891 "tenferro-fft",
892 self.axis,
893 input_shape.len(),
894 ));
895 }
896
897 let mut out_shape = input_shape.to_vec();
898 let output_dtype = match self.operation {
899 FftOperation::C2cForward | FftOperation::C2cInverse => {
900 if !matches!(input_dtype, DType::C32 | DType::C64) {
901 return Err(tensor_unsupported_dtype(
902 "tenferro-fft",
903 input_dtype,
904 "C32 or C64",
905 ));
906 }
907 input_dtype
908 }
909 FftOperation::R2cFull | FftOperation::R2cOnesided => {
910 let len = transform_len_dim(self.n, &input_shape[self.axis]);
911 out_shape[self.axis] = if self.operation.is_onesided() {
912 len / 2usize + 1usize
913 } else {
914 len
915 };
916 match input_dtype {
917 DType::F32 => DType::C32,
918 DType::F64 => DType::C64,
919 _ => {
920 return Err(tensor_unsupported_dtype(
921 "tenferro-fft",
922 input_dtype,
923 "F32 or F64",
924 ));
925 }
926 }
927 }
928 FftOperation::C2r => {
929 out_shape[self.axis] = output_dim_c2r(&input_shape[self.axis], self.n)?;
930 match input_dtype {
931 DType::C32 => DType::F32,
932 DType::C64 => DType::F64,
933 _ => {
934 return Err(tensor_unsupported_dtype(
935 "tenferro-fft",
936 input_dtype,
937 "C32 or C64",
938 ));
939 }
940 }
941 }
942 };
943
944 if self.operation.is_c2c() {
945 out_shape[self.axis] = transform_len_dim(self.n, &input_shape[self.axis]);
946 }
947
948 Ok(vec![(output_dtype, out_shape)])
949 }
950}
951
952fn execute_concrete_fft_op<B: FftBackend>(
953 input: &Tensor,
954 spec: &FftPlanSpec,
955 backend: &mut B,
956) -> tenferro_tensor::Result<Tensor> {
957 let mut plans = FftPlanCache::with_capacity(NonZeroUsize::MIN);
958 backend.execute_fft(input, spec, FftExecutionCache::caller_owned(&mut plans))
959}
960
961#[allow(clippy::too_many_arguments)]
962fn execute_concrete_fft_read_op<B: FftBackend>(
963 input: &TensorRead<'_>,
964 operation: FftOperation,
965 op_name: &'static str,
966 n: Option<usize>,
967 axis: isize,
968 norm: FftNorm,
969 backend: &mut B,
970) -> tenferro_tensor::Result<Tensor> {
971 let spec = concrete_fft_spec(
972 op_name,
973 operation,
974 input.dtype(),
975 input.shape(),
976 n,
977 axis,
978 norm,
979 )?;
980 let materialized = backend.to_contiguous_read(input.clone())?;
981 let mut plans = FftPlanCache::with_capacity(NonZeroUsize::MIN);
982 backend.execute_fft(
983 &materialized,
984 &spec,
985 FftExecutionCache::caller_owned(&mut plans),
986 )
987}
988
989#[allow(clippy::too_many_arguments)]
990fn concrete_fft_spec(
991 op: &'static str,
992 operation: FftOperation,
993 input_dtype: DType,
994 input_shape: &[usize],
995 n: Option<usize>,
996 axis: isize,
997 norm: FftNorm,
998) -> tenferro_tensor::Result<FftPlanSpec> {
999 validate_concrete_n(op, n)?;
1000 let axis = normalize_concrete_axis(op, axis, input_shape.len())?;
1001 validated_fft_plan_spec(op, operation, input_dtype, input_shape, n, axis, norm)
1002}
1003
1004#[allow(clippy::too_many_arguments)]
1005fn validated_fft_plan_spec(
1006 op: &'static str,
1007 operation: FftOperation,
1008 input_dtype: DType,
1009 input_shape: &[usize],
1010 n: Option<usize>,
1011 axis: usize,
1012 norm: FftNorm,
1013) -> tenferro_tensor::Result<FftPlanSpec> {
1014 validate_concrete_n(op, n)?;
1015 validate_operation_dtype(op, operation, input_dtype)?;
1016 validate_axis(op, input_shape, axis)?;
1017 validate_concrete_transform_len(op, input_shape, n, axis)?;
1018 if operation == FftOperation::C2r {
1019 output_shape_c2r(input_shape, axis, n)?;
1020 }
1021 Ok(FftPlanSpec::new(
1022 operation,
1023 axis,
1024 n,
1025 norm,
1026 input_dtype,
1027 input_shape.to_vec(),
1028 ))
1029}
1030
1031fn concrete_fft_operation(op: &'static str, dtype: DType) -> tenferro_tensor::Result<FftOperation> {
1032 match dtype {
1033 DType::C32 | DType::C64 => Ok(FftOperation::C2cForward),
1034 DType::F32 | DType::F64 => Ok(FftOperation::R2cFull),
1035 DType::I32 | DType::I64 | DType::Bool => {
1036 Err(tensor_unsupported_dtype(op, dtype, "F32, F64, C32, or C64"))
1037 }
1038 }
1039}
1040
1041fn concrete_ifft_operation(
1042 op: &'static str,
1043 dtype: DType,
1044) -> tenferro_tensor::Result<FftOperation> {
1045 match dtype {
1046 DType::C32 | DType::C64 => Ok(FftOperation::C2cInverse),
1047 DType::F32 | DType::F64 | DType::I32 | DType::I64 | DType::Bool => {
1048 Err(tensor_unsupported_dtype(op, dtype, "C32 or C64"))
1049 }
1050 }
1051}
1052
1053fn concrete_rfft_operation(
1054 op: &'static str,
1055 dtype: DType,
1056) -> tenferro_tensor::Result<FftOperation> {
1057 match dtype {
1058 DType::F32 | DType::F64 => Ok(FftOperation::R2cOnesided),
1059 DType::C32 | DType::C64 | DType::I32 | DType::I64 | DType::Bool => {
1060 Err(tensor_unsupported_dtype(op, dtype, "F32 or F64"))
1061 }
1062 }
1063}
1064
1065fn concrete_irfft_operation(
1066 op: &'static str,
1067 dtype: DType,
1068) -> tenferro_tensor::Result<FftOperation> {
1069 match dtype {
1070 DType::C32 | DType::C64 => Ok(FftOperation::C2r),
1071 DType::F32 | DType::F64 | DType::I32 | DType::I64 | DType::Bool => {
1072 Err(tensor_unsupported_dtype(op, dtype, "C32 or C64"))
1073 }
1074 }
1075}
1076
1077fn validate_operation_dtype(
1078 op: &'static str,
1079 operation: FftOperation,
1080 dtype: DType,
1081) -> tenferro_tensor::Result<()> {
1082 let supported = match operation {
1083 FftOperation::C2cForward | FftOperation::C2cInverse | FftOperation::C2r => {
1084 matches!(dtype, DType::C32 | DType::C64)
1085 }
1086 FftOperation::R2cFull | FftOperation::R2cOnesided => {
1087 matches!(dtype, DType::F32 | DType::F64)
1088 }
1089 };
1090 if supported {
1091 Ok(())
1092 } else {
1093 Err(tensor_unsupported_dtype(
1094 op,
1095 dtype,
1096 expected_dtype_description(operation),
1097 ))
1098 }
1099}
1100
1101fn validate_concrete_n(op: &'static str, n: Option<usize>) -> tenferro_tensor::Result<()> {
1102 if n == Some(0) {
1103 return Err(tenferro_tensor::Error::invalid_argument(
1104 op,
1105 "n",
1106 "transform length must be positive",
1107 ));
1108 }
1109 Ok(())
1110}
1111
1112fn validate_concrete_transform_len(
1113 op: &'static str,
1114 input_shape: &[usize],
1115 n: Option<usize>,
1116 axis: usize,
1117) -> tenferro_tensor::Result<()> {
1118 if n.is_none() && input_shape.get(axis).copied() == Some(0) {
1119 return Err(tenferro_tensor::Error::invalid_argument(
1120 op,
1121 "n",
1122 "transform length must be positive",
1123 ));
1124 }
1125 Ok(())
1126}
1127
1128fn normalize_concrete_axis(
1129 op: &'static str,
1130 axis: isize,
1131 rank: usize,
1132) -> tenferro_tensor::Result<usize> {
1133 if rank == 0 {
1134 return Err(tenferro_tensor::Error::invalid_argument(
1135 op,
1136 "rank",
1137 "FFT requires rank >= 1",
1138 ));
1139 }
1140 let normalized = if axis >= 0 {
1141 axis as usize
1142 } else {
1143 rank.checked_sub(axis.unsigned_abs()).ok_or_else(|| {
1144 tenferro_tensor::Error::axis_out_of_bounds(op, axis.unsigned_abs(), rank)
1145 })?
1146 };
1147 if normalized >= rank {
1148 return Err(tenferro_tensor::Error::axis_out_of_bounds(
1149 op, normalized, rank,
1150 ));
1151 }
1152 Ok(normalized)
1153}
1154
1155fn tensor_unsupported_dtype(
1156 op: &'static str,
1157 dtype: DType,
1158 expected: &'static str,
1159) -> tenferro_tensor::Error {
1160 tenferro_tensor::Error::extension(
1161 op,
1162 FFT_EXTENSION_FAMILY_ID,
1163 ErrorKind::Unsupported,
1164 FftError::UnsupportedDType {
1165 op,
1166 dtype,
1167 expected,
1168 },
1169 )
1170}
1171
1172#[cfg(feature = "autodiff")]
1173#[derive(Debug)]
1174struct FftAdRule;
1175
1176#[cfg(feature = "autodiff")]
1177impl SemanticLinearizeRule for FftAdRule {
1178 fn family_id(&self) -> &'static str {
1179 FFT_EXTENSION_FAMILY_ID
1180 }
1181
1182 fn linearize(
1183 &self,
1184 request: SemanticLinearizeRequest<'_>,
1185 builder: &mut SemanticProgramBuilder,
1186 ) -> std::result::Result<SemanticLinearizeResult, SemanticAdError> {
1187 let fft_op = semantic_fft_payload(request.op(), SemanticAdRuleKind::Linearize)?;
1188 if !fft_op.operation.is_c2c() {
1189 return Err(semantic_fft_unsupported(
1190 fft_op.operation,
1191 SemanticAdRuleKind::Linearize,
1192 ));
1193 }
1194 let tangent = match request.tangent_inputs()[0] {
1195 AdValue::Absent => AdValue::Absent,
1196 AdValue::Value(tangent) => {
1197 AdValue::Value(builder.add_extension(Arc::new(fft_op.clone()), &[tangent])?[0])
1198 }
1199 };
1200 Ok(SemanticLinearizeResult::new([tangent], []))
1201 }
1202}
1203
1204#[cfg(feature = "autodiff")]
1205impl SemanticLinearTransposeRule for FftAdRule {
1206 fn family_id(&self) -> &'static str {
1207 FFT_EXTENSION_FAMILY_ID
1208 }
1209
1210 fn linear_transpose(
1211 &self,
1212 request: SemanticLinearTransposeRequest<'_>,
1213 builder: &mut SemanticProgramBuilder,
1214 ) -> std::result::Result<Box<[AdValue]>, SemanticAdError> {
1215 Ok([semantic_fft_adjoint(
1216 request.op(),
1217 request.cotangent_outputs()[0],
1218 request.active_inputs()[0],
1219 request.primal_inputs()[0],
1220 builder,
1221 )?]
1222 .into())
1223 }
1224}
1225
1226#[cfg(feature = "autodiff")]
1227impl SemanticPrimalVjpRule for FftAdRule {
1228 fn family_id(&self) -> &'static str {
1229 FFT_EXTENSION_FAMILY_ID
1230 }
1231
1232 fn primal_vjp(
1233 &self,
1234 request: SemanticPrimalVjpRequest<'_>,
1235 builder: &mut SemanticProgramBuilder,
1236 ) -> std::result::Result<Box<[AdValue]>, SemanticAdError> {
1237 Ok([semantic_fft_adjoint(
1238 request.op(),
1239 request.cotangent_outputs()[0],
1240 request.active_inputs()[0],
1241 request.primal_inputs()[0],
1242 builder,
1243 )?]
1244 .into())
1245 }
1246}
1247
1248#[cfg(feature = "autodiff")]
1249#[derive(Clone, Copy)]
1250enum SemanticAdRuleKind {
1251 Linearize,
1252 Transpose,
1253}
1254
1255#[cfg(feature = "autodiff")]
1256fn semantic_fft_payload(
1257 op: &dyn ExtensionOp,
1258 role: SemanticAdRuleKind,
1259) -> std::result::Result<&FftOp, SemanticAdError> {
1260 op.as_any().downcast_ref::<FftOp>().ok_or_else(|| {
1261 semantic_fft_unsupported_family(
1262 FFT_EXTENSION_FAMILY_ID,
1263 role,
1264 "FFT semantic AD received an incompatible extension payload",
1265 )
1266 })
1267}
1268
1269#[cfg(feature = "autodiff")]
1270fn semantic_fft_adjoint(
1271 op: &dyn ExtensionOp,
1272 cotangent: AdValue,
1273 active: bool,
1274 primal_input: ProgramValue,
1275 builder: &mut SemanticProgramBuilder,
1276) -> std::result::Result<AdValue, SemanticAdError> {
1277 if !active {
1278 return Ok(AdValue::Absent);
1279 }
1280 let AdValue::Value(cotangent) = cotangent else {
1281 return Ok(AdValue::Absent);
1282 };
1283 let fft_op = semantic_fft_payload(op, SemanticAdRuleKind::Transpose)?;
1284 if !fft_op.operation.is_c2c() {
1285 return Err(semantic_fft_unsupported(
1286 fft_op.operation,
1287 SemanticAdRuleKind::Transpose,
1288 ));
1289 }
1290 let adjoint_op = fft_op
1291 .c2c_adjoint()
1292 .ok_or_else(|| semantic_fft_unsupported(fft_op.operation, SemanticAdRuleKind::Transpose))?;
1293 let adjoint = builder.add_extension(Arc::new(adjoint_op), &[cotangent])?[0];
1294 restore_semantic_c2c_adjoint_input_length(builder, adjoint, primal_input, fft_op)
1295 .map(AdValue::Value)
1296}
1297
1298#[cfg(feature = "autodiff")]
1299fn restore_semantic_c2c_adjoint_input_length(
1300 builder: &mut SemanticProgramBuilder,
1301 adjoint: ProgramValue,
1302 primal_input: ProgramValue,
1303 fft_op: &FftOp,
1304) -> std::result::Result<ProgramValue, SemanticAdError> {
1305 let Some(transform_len) = fft_op.n else {
1306 return Ok(adjoint);
1307 };
1308 let input_len = builder
1309 .value_metadata(primal_input)?
1310 .shape()
1311 .get(fft_op.axis)
1312 .and_then(|extent| extent.as_exact())
1313 .and_then(|dim| match dim {
1314 tenferro_ops::dim_expr::DimExpr::Const(value) => Some(*value),
1315 _ => None,
1316 });
1317 if input_len == Some(transform_len) {
1318 return Ok(adjoint);
1319 }
1320
1321 let size = builder.add_op(
1322 CoreSemanticOp::ShapeOf { axis: fft_op.axis },
1323 &[primal_input],
1324 )?[0];
1325 let truncated = builder.add_op(
1326 CoreSemanticOp::DynamicTruncate { axis: fft_op.axis },
1327 &[adjoint, size],
1328 )?[0];
1329 Ok(builder.add_op(
1330 CoreSemanticOp::PadToMatch { axis: fft_op.axis },
1331 &[truncated, primal_input],
1332 )?[0])
1333}
1334
1335#[cfg(feature = "autodiff")]
1336fn semantic_fft_unsupported(operation: FftOperation, role: SemanticAdRuleKind) -> SemanticAdError {
1337 semantic_fft_unsupported_family(
1338 fft_ad_family_id(operation),
1339 role,
1340 "FFT operation has no semantic AD rule",
1341 )
1342}
1343
1344#[cfg(feature = "autodiff")]
1345fn semantic_fft_unsupported_family(
1346 family_id: &'static str,
1347 role: SemanticAdRuleKind,
1348 message: impl Into<String>,
1349) -> SemanticAdError {
1350 SemanticAdError::Unsupported {
1351 family_id,
1352 role: match role {
1353 SemanticAdRuleKind::Linearize => {
1354 tenferro_ad::semantic_extension::SemanticAdRuleRole::Linearize
1355 }
1356 SemanticAdRuleKind::Transpose => {
1357 tenferro_ad::semantic_extension::SemanticAdRuleRole::LinearTranspose
1358 }
1359 },
1360 message: message.into(),
1361 }
1362}
1363
1364#[cfg(feature = "autodiff")]
1366pub fn semantic_ad_rules(
1374) -> std::result::Result<SemanticExtensionRuleSet, SemanticExtensionRegistryError> {
1375 SemanticExtensionRuleSet::new()
1376 .with_linearize(Arc::new(FftAdRule))?
1377 .with_linear_transpose(Arc::new(FftAdRule))?
1378 .with_primal_vjp(Arc::new(FftAdRule))
1379}
1380
1381pub(crate) fn execute_fft_extension_reads_owner<B: TensorBackend + 'static>(
1382 op: &FftOp,
1383 inputs: &[TensorRead<'_>],
1384 ctx: &mut ExtensionExecutionContext<'_, B>,
1385) -> tenferro_tensor::Result<Vec<Tensor>> {
1386 let (backend, caches) = ctx.parts_mut();
1387 backend.with_backend_session(|session| {
1388 execute_fft_extension_reads_on_session(op, inputs, session, caches)
1389 })
1390}
1391
1392#[cfg(feature = "autodiff")]
1393pub(crate) fn execute_fft_extension_reads_session(
1394 op: &FftOp,
1395 inputs: &[TensorRead<'_>],
1396 ctx: &mut ExtensionExecutionContext<'_, dyn BackendSession + '_>,
1397) -> tenferro_tensor::Result<Vec<Tensor>> {
1398 let (session, caches) = ctx.parts_mut();
1399 execute_fft_extension_reads_on_session(op, inputs, session, caches)
1400}
1401
1402fn execute_fft_extension_for_capability<B: FftBackend + ?Sized>(
1403 op: &FftOp,
1404 inputs: &[&Tensor],
1405 session: &mut B,
1406 caches: &mut ExtensionCacheStore,
1407) -> tenferro_tensor::Result<Vec<Tensor>> {
1408 if inputs.len() != 1 {
1409 return Err(tenferro_tensor::Error::invalid_argument(
1410 "tenferro-fft",
1411 "inputs",
1412 format!("expected 1 input, got {}", inputs.len()),
1413 ));
1414 }
1415 let input = inputs[0];
1416 let spec = validated_fft_plan_spec(
1417 fft_op_name(op.operation),
1418 op.operation,
1419 input.dtype(),
1420 input.shape(),
1421 op.n,
1422 op.axis,
1423 op.norm,
1424 )?;
1425 let output = session.execute_fft(input, &spec, FftExecutionCache::runtime_owned(caches))?;
1426 Ok(vec![output])
1427}
1428
1429fn execute_fft_extension_reads_on_session(
1430 op: &FftOp,
1431 inputs: &[TensorRead<'_>],
1432 session: &mut dyn BackendSession,
1433 caches: &mut ExtensionCacheStore,
1434) -> tenferro_tensor::Result<Vec<Tensor>> {
1435 if let Some(result) = with_cpu_exec_session(session, |session| {
1436 execute_fft_extension_reads_for_capability(op, inputs, session, caches)
1437 }) {
1438 return result;
1439 }
1440 #[cfg(feature = "webgpu")]
1441 if let Some(result) = with_webgpu_exec_session(session, |session| {
1442 execute_fft_extension_reads_for_capability(op, inputs, session, caches)
1443 }) {
1444 return result;
1445 }
1446 Err(tenferro_tensor::Error::unsupported(
1447 fft_op_name(op.operation),
1448 "selected backend session does not expose an FFT execution capability",
1449 ))
1450}
1451
1452fn execute_fft_extension_reads_for_capability<B: FftBackend + ?Sized>(
1453 op: &FftOp,
1454 inputs: &[TensorRead<'_>],
1455 session: &mut B,
1456 caches: &mut ExtensionCacheStore,
1457) -> tenferro_tensor::Result<Vec<Tensor>> {
1458 let op_name = fft_op_name(op.operation);
1459 for input in inputs {
1460 session.validate_fft_read_input(op_name, input)?;
1461 }
1462 let materialized_inputs = inputs
1463 .iter()
1464 .cloned()
1465 .map(|input| session.to_contiguous_read(input))
1466 .collect::<tenferro_tensor::Result<Vec<_>>>()?;
1467 let input_refs: Vec<&Tensor> = materialized_inputs.iter().collect();
1468 execute_fft_extension_for_capability(op, &input_refs, session, caches)
1469}
1470
1471define_extension_runtime! {
1472 runtime = FftRuntime,
1473 family_id = FFT_EXTENSION_FAMILY_ID,
1474 op_type = FftOp,
1475 execute = execute_fft_extension_reads_owner,
1476 execute_reads = execute_fft_extension_reads_owner,
1477 backend_bound = TensorBackend,
1478}
1479
1480fn fft(input: &TracedTensor, n: Option<usize>, axis: isize, norm: FftNorm) -> Result<TracedTensor> {
1512 let operation = runtime_forward_fft_operation(input.dtype)?;
1513 apply_unary_fft("fft", input, operation, n, axis, norm)
1514}
1515
1516fn ifft(
1545 input: &TracedTensor,
1546 n: Option<usize>,
1547 axis: isize,
1548 norm: FftNorm,
1549) -> Result<TracedTensor> {
1550 require_runtime_dtype("ifft", input.dtype, &[DType::C32, DType::C64], "C32 or C64")?;
1551 apply_unary_fft("ifft", input, FftOperation::C2cInverse, n, axis, norm)
1552}
1553
1554fn rfft(
1587 input: &TracedTensor,
1588 n: Option<usize>,
1589 axis: isize,
1590 norm: FftNorm,
1591) -> Result<TracedTensor> {
1592 require_runtime_dtype("rfft", input.dtype, &[DType::F32, DType::F64], "F32 or F64")?;
1593 apply_unary_fft("rfft", input, FftOperation::R2cOnesided, n, axis, norm)
1594}
1595
1596fn irfft(
1632 input: &TracedTensor,
1633 n: Option<usize>,
1634 axis: isize,
1635 norm: FftNorm,
1636) -> Result<TracedTensor> {
1637 require_runtime_dtype(
1638 "irfft",
1639 input.dtype,
1640 &[DType::C32, DType::C64],
1641 "C32 or C64",
1642 )?;
1643 apply_unary_fft("irfft", input, FftOperation::C2r, n, axis, norm)
1644}
1645
1646fn apply_unary_fft(
1647 op_name: &'static str,
1648 input: &TracedTensor,
1649 operation: FftOperation,
1650 n: Option<usize>,
1651 axis: isize,
1652 norm: FftNorm,
1653) -> Result<TracedTensor> {
1654 let concrete_shape = input.try_concrete_shape();
1655 let op = Arc::new(prepare_runtime_fft_op(
1656 op_name,
1657 operation,
1658 input.rank,
1659 concrete_shape.as_deref(),
1660 n,
1661 axis,
1662 norm,
1663 )?);
1664 let mut outputs = apply(op, &[input])?;
1665 outputs
1666 .pop()
1667 .ok_or_else(|| Error::Internal("FFT extension declares exactly one output".into()))
1668}
1669
1670fn normalize_axis(op: &'static str, axis: isize, rank: usize) -> Result<usize> {
1671 if rank == 0 {
1672 return Err(runtime_invalid_argument(
1673 op,
1674 "rank",
1675 "FFT requires rank >= 1",
1676 ));
1677 }
1678 let normalized = if axis >= 0 {
1679 axis as usize
1680 } else {
1681 rank.checked_sub(axis.unsigned_abs())
1682 .ok_or_else(|| runtime_axis_out_of_bounds(op, axis.unsigned_abs(), rank))?
1683 };
1684 if normalized >= rank {
1685 return Err(runtime_axis_out_of_bounds(op, normalized, rank));
1686 }
1687 Ok(normalized)
1688}
1689
1690fn validate_n(op: &'static str, n: Option<usize>) -> Result<()> {
1691 if n == Some(0) {
1692 return Err(runtime_invalid_argument(
1693 op,
1694 "n",
1695 "transform length must be positive",
1696 ));
1697 }
1698 Ok(())
1699}
1700
1701fn prepare_runtime_fft_op(
1702 op: &'static str,
1703 operation: FftOperation,
1704 rank: usize,
1705 concrete_shape: Option<&[usize]>,
1706 n: Option<usize>,
1707 axis: isize,
1708 norm: FftNorm,
1709) -> Result<FftOp> {
1710 validate_n(op, n)?;
1711 let axis = normalize_axis(op, axis, rank)?;
1712 if n.is_none() && concrete_shape.and_then(|shape| shape.get(axis).copied()) == Some(0) {
1713 return Err(runtime_invalid_argument(
1714 op,
1715 "n",
1716 "transform length must be positive",
1717 ));
1718 }
1719 if operation == FftOperation::C2r {
1720 if let Some(shape) = concrete_shape {
1721 output_shape_c2r(shape, axis, n)?;
1722 }
1723 }
1724 Ok(FftOp::new(operation, axis, n, norm))
1725}
1726
1727fn runtime_forward_fft_operation(dtype: DType) -> Result<FftOperation> {
1728 match dtype {
1729 DType::C32 | DType::C64 => Ok(FftOperation::C2cForward),
1730 DType::F32 | DType::F64 => Ok(FftOperation::R2cFull),
1731 DType::I32 | DType::I64 | DType::Bool => Err(runtime_unsupported_dtype(
1732 "fft",
1733 dtype,
1734 "F32, F64, C32, or C64",
1735 )),
1736 }
1737}
1738
1739fn require_runtime_dtype(
1740 op: &'static str,
1741 dtype: DType,
1742 supported: &[DType],
1743 expected: &'static str,
1744) -> Result<()> {
1745 if supported.contains(&dtype) {
1746 Ok(())
1747 } else {
1748 Err(runtime_unsupported_dtype(op, dtype, expected))
1749 }
1750}
1751
1752fn runtime_invalid_argument(
1753 op: &'static str,
1754 argument: &'static str,
1755 message: impl Into<String>,
1756) -> Error {
1757 Error::validation(
1758 op,
1759 ErrorPhase::GraphBuild,
1760 ValidationError::InvalidArgument {
1761 argument,
1762 message: message.into(),
1763 },
1764 )
1765}
1766
1767fn runtime_axis_out_of_bounds(op: &'static str, axis: usize, rank: usize) -> Error {
1768 Error::validation(
1769 op,
1770 ErrorPhase::GraphBuild,
1771 ValidationError::AxisOutOfBounds { axis, rank },
1772 )
1773}
1774
1775fn runtime_unsupported_dtype(op: &'static str, dtype: DType, expected: &'static str) -> Error {
1776 Error::extension(
1777 op,
1778 ErrorPhase::GraphBuild,
1779 FFT_EXTENSION_FAMILY_ID,
1780 ErrorKind::Unsupported,
1781 FftError::UnsupportedDType {
1782 op,
1783 dtype,
1784 expected,
1785 },
1786 )
1787}
1788
1789fn transform_len_dim(n: Option<usize>, input_dim: &SymDim) -> SymDim {
1790 n.map(SymDim::from).unwrap_or_else(|| input_dim.clone())
1791}
1792
1793fn expected_dtype_description(operation: FftOperation) -> &'static str {
1794 match operation {
1795 FftOperation::C2cForward | FftOperation::C2cInverse | FftOperation::C2r => "C32 or C64",
1796 FftOperation::R2cFull | FftOperation::R2cOnesided => "F32 or F64",
1797 }
1798}
1799
1800fn fft_op_name(operation: FftOperation) -> &'static str {
1801 match operation {
1802 FftOperation::C2cForward => "fft",
1803 FftOperation::C2cInverse => "ifft",
1804 FftOperation::R2cFull | FftOperation::R2cOnesided => "rfft",
1805 FftOperation::C2r => "irfft",
1806 }
1807}
1808
1809#[cfg(feature = "autodiff")]
1810fn fft_ad_family_id(operation: FftOperation) -> &'static str {
1811 match operation {
1812 FftOperation::C2cForward | FftOperation::C2cInverse => FFT_EXTENSION_FAMILY_ID,
1813 FftOperation::R2cFull | FftOperation::R2cOnesided => "tenferro-fft.rfft.v1",
1814 FftOperation::C2r => "tenferro-fft.irfft.v1",
1815 }
1816}
1817
1818fn output_shape_c2c(
1819 shape: &[usize],
1820 axis: usize,
1821 n: Option<usize>,
1822) -> tenferro_tensor::Result<Vec<usize>> {
1823 let len = transform_len(shape, axis, n)?;
1824 let mut out_shape = shape.to_vec();
1825 out_shape[axis] = len;
1826 Ok(out_shape)
1827}
1828
1829fn output_shape_r2c(
1830 shape: &[usize],
1831 axis: usize,
1832 n: Option<usize>,
1833 onesided: bool,
1834) -> tenferro_tensor::Result<Vec<usize>> {
1835 let len = transform_len(shape, axis, n)?;
1836 let mut out_shape = shape.to_vec();
1837 out_shape[axis] = if onesided { len / 2 + 1 } else { len };
1838 Ok(out_shape)
1839}
1840
1841fn output_shape_c2r(
1842 shape: &[usize],
1843 axis: usize,
1844 n: Option<usize>,
1845) -> tenferro_tensor::Result<Vec<usize>> {
1846 validate_axis("irfft", shape, axis)?;
1847 let input_len = shape[axis];
1848 let len = match n {
1849 Some(len) => len,
1850 None => default_c2r_output_len(input_len)?,
1851 };
1852 if len == 0 {
1853 return Err(tenferro_tensor::Error::invalid_argument(
1854 "irfft",
1855 "output length",
1856 "must be positive",
1857 ));
1858 }
1859 validate_c2r_spectrum_len(input_len, len)?;
1860 let mut out_shape = shape.to_vec();
1861 out_shape[axis] = len;
1862 Ok(out_shape)
1863}
1864
1865fn output_dim_c2r(input_dim: &SymDim, n: Option<usize>) -> tenferro_tensor::Result<SymDim> {
1866 match (input_dim.constant_value(), n) {
1867 (Some(input_len), Some(output_len)) => {
1868 if output_len == 0 {
1869 return Err(tenferro_tensor::Error::invalid_argument(
1870 "irfft",
1871 "output length",
1872 "must be positive",
1873 ));
1874 }
1875 validate_c2r_spectrum_len(input_len, output_len)?;
1876 Ok(SymDim::from(output_len))
1877 }
1878 (Some(input_len), None) => Ok(SymDim::from(default_c2r_output_len(input_len)?)),
1879 (None, Some(output_len)) => {
1880 if output_len == 0 {
1881 return Err(tenferro_tensor::Error::invalid_argument(
1882 "irfft",
1883 "output length",
1884 "must be positive",
1885 ));
1886 }
1887 Ok(SymDim::from(output_len))
1888 }
1889 (None, None) => Ok((input_dim.clone() - 1usize) * 2usize),
1890 }
1891}
1892
1893fn default_c2r_output_len(input_len: usize) -> tenferro_tensor::Result<usize> {
1894 if input_len == 0 {
1895 return Err(tenferro_tensor::Error::invalid_argument(
1896 "irfft",
1897 "input spectrum axis length",
1898 "must be positive",
1899 ));
1900 }
1901 input_len
1902 .checked_sub(1)
1903 .and_then(|len| len.checked_mul(2))
1904 .ok_or_else(|| {
1905 tenferro_tensor::Error::invalid_argument(
1906 "irfft",
1907 "default output length",
1908 "overflows usize",
1909 )
1910 })
1911}
1912
1913fn validate_c2r_spectrum_len(
1914 input_len: usize,
1915 output_len: usize,
1916) -> tenferro_tensor::Result<usize> {
1917 let expected = output_len / 2 + 1;
1918 if input_len != expected {
1919 return Err(tenferro_tensor::Error::invalid_argument(
1920 "irfft",
1921 "spectrum",
1922 format!(
1923 "one-sided spectrum axis length mismatch: expected {expected} for output length {output_len}, got {input_len}"
1924 ),
1925 ));
1926 }
1927 Ok(expected)
1928}
1929
1930fn transform_len(shape: &[usize], axis: usize, n: Option<usize>) -> tenferro_tensor::Result<usize> {
1931 validate_axis("fft", shape, axis)?;
1932 let len = n.unwrap_or(shape[axis]);
1933 if len == 0 {
1934 return Err(tenferro_tensor::Error::invalid_argument(
1935 "fft",
1936 "transform length",
1937 "must be positive",
1938 ));
1939 }
1940 Ok(len)
1941}
1942
1943fn validate_axis(op: &'static str, shape: &[usize], axis: usize) -> tenferro_tensor::Result<()> {
1944 if axis >= shape.len() {
1945 return Err(tenferro_tensor::Error::axis_out_of_bounds(
1946 op,
1947 axis,
1948 shape.len(),
1949 ));
1950 }
1951 Ok(())
1952}
1953
1954#[cfg(test)]
1955mod concrete_tests;
1956
1957#[cfg(test)]
1958mod tests {
1959 use super::*;
1960
1961 #[test]
1962 fn fft_infer_output_meta_rejects_invalid_trait_inputs_without_panicking() {
1963 let op = FftOp::new(FftOperation::R2cOnesided, 0, None, FftNorm::Backward);
1964 let shape = [SymDim::from(4usize)];
1965
1966 assert!(
1967 tenferro_ops::ext_op::invoke_extension_shape_inference(&op, &[], &[&shape]).is_err()
1968 );
1969 assert!(
1970 tenferro_ops::ext_op::invoke_extension_shape_inference(&op, &[DType::F64], &[])
1971 .is_err()
1972 );
1973 assert!(tenferro_ops::ext_op::invoke_extension_shape_inference(
1974 &op,
1975 &[DType::I64],
1976 &[&shape]
1977 )
1978 .is_err());
1979
1980 let bad_axis = FftOp::new(FftOperation::C2cForward, 2, None, FftNorm::Backward);
1981 assert!(tenferro_ops::ext_op::invoke_extension_shape_inference(
1982 &bad_axis,
1983 &[DType::C64],
1984 &[&shape]
1985 )
1986 .is_err());
1987 }
1988
1989 #[test]
1990 fn checked_shape_product_rejects_overflow_before_allocation() {
1991 let err = cpu::checked_shape_product("fft", "output", &[usize::MAX, 2])
1992 .expect_err("overflowing output shape should be rejected");
1993
1994 assert!(err.to_string().contains("overflows usize"), "{err}");
1995 }
1996
1997 #[test]
1998 fn irfft_default_output_length_rejects_overflow() {
1999 let err = output_shape_c2r(&[usize::MAX], 0, None)
2000 .expect_err("default irfft output length should reject overflow");
2001
2002 assert!(err.to_string().contains("overflows usize"), "{err}");
2003 }
2004
2005 #[test]
2006 fn normalize_axis_handles_large_rank_without_isize_cast_wrap() {
2007 assert_eq!(normalize_axis("fft", 0, usize::MAX).unwrap(), 0);
2008 assert_eq!(
2009 normalize_axis("fft", -1, usize::MAX).unwrap(),
2010 usize::MAX - 1
2011 );
2012 assert!(normalize_axis("fft", isize::MIN, 3).is_err());
2013 }
2014
2015 #[test]
2016 fn axis_lane_layout_rejects_stride_overflow() {
2017 let err = cpu::for_axis_lane(&[usize::MAX, 2], 1, 2, |_| Ok(()))
2018 .expect_err("lane layout should reject stride overflow");
2019
2020 assert!(err.to_string().contains("overflows usize"), "{err}");
2021 }
2022
2023 #[cfg(feature = "autodiff")]
2024 #[test]
2025 fn fft_semantic_rules_emit_extension_first_jvp_and_length_restoring_transpose() {
2026 use tenferro_ops::dim_expr::DimExpr;
2027 use tenferro_runtime::program::{ProgramInputSpec, SemanticOpRef, SemanticProgramBuilder};
2028
2029 let fft_op = FftOp::new(FftOperation::C2cForward, 0, Some(2), FftNorm::Backward);
2030 let mut source = SemanticProgramBuilder::new();
2031 let source_input = source
2032 .input(ProgramInputSpec::new(DType::C64, [DimExpr::Const(4)]))
2033 .unwrap();
2034 let source_output = source
2035 .add_extension(Arc::new(fft_op), &[source_input])
2036 .unwrap()[0];
2037 let source = source.finish(&[source_output]).unwrap();
2038 let operation = source.program.operations().next().unwrap();
2039
2040 let rules = semantic_ad_rules().unwrap();
2041 let mut destination = SemanticProgramBuilder::new();
2042 let primal = destination
2043 .input(ProgramInputSpec::new(DType::C64, [DimExpr::Const(4)]))
2044 .unwrap();
2045 let tangent = destination
2046 .input(ProgramInputSpec::new(DType::C64, [DimExpr::Const(4)]))
2047 .unwrap();
2048 let primal_output = destination
2049 .add_extension(
2050 Arc::new(FftOp::new(
2051 FftOperation::C2cForward,
2052 0,
2053 Some(2),
2054 FftNorm::Backward,
2055 )),
2056 &[primal],
2057 )
2058 .unwrap()[0];
2059 let linearized = rules
2060 .linearize_operation(
2061 operation,
2062 &[primal],
2063 &[primal_output],
2064 &[AdValue::Value(tangent)],
2065 &[true],
2066 &mut destination,
2067 )
2068 .unwrap();
2069 let AdValue::Value(tangent_output) = linearized.tangent_outputs()[0] else {
2070 panic!("FFT tangent must be active");
2071 };
2072 let cotangent_inputs = rules
2073 .linear_transpose_operation(
2074 operation,
2075 &[primal],
2076 &[primal_output],
2077 &[AdValue::Value(tangent_output)],
2078 &[true],
2079 linearized.residuals(),
2080 &mut destination,
2081 )
2082 .unwrap();
2083 let AdValue::Value(cotangent_input) = cotangent_inputs[0] else {
2084 panic!("FFT cotangent must be active");
2085 };
2086 let frozen = destination
2087 .finish(&[tangent_output, cotangent_input])
2088 .unwrap();
2089 let operations: Vec<_> = frozen.program.operations().collect();
2090 assert!(
2091 operations
2092 .iter()
2093 .filter(|operation| matches!(operation.op(), SemanticOpRef::Extension(_)))
2094 .count()
2095 >= 3
2096 );
2097 assert!(operations.iter().any(|operation| matches!(
2098 operation.op(),
2099 SemanticOpRef::Core(CoreSemanticOp::DynamicTruncate { axis: 0 })
2100 )));
2101 assert!(operations.iter().any(|operation| matches!(
2102 operation.op(),
2103 SemanticOpRef::Core(CoreSemanticOp::PadToMatch { axis: 0 })
2104 )));
2105 }
2106
2107 #[cfg(feature = "autodiff")]
2108 #[test]
2109 fn fft_semantic_rules_run_through_whole_program_jvp_and_vjp() {
2110 use tenferro_ad::AdContext;
2111 use tenferro_ops::dim_expr::DimExpr;
2112 use tenferro_runtime::program::{ProgramInputSpec, SemanticOpRef, SemanticProgramBuilder};
2113
2114 let mut builder = SemanticProgramBuilder::new();
2115 let input = builder
2116 .input(ProgramInputSpec::new(DType::C64, [DimExpr::Const(4)]))
2117 .unwrap();
2118 let output = builder
2119 .add_extension(
2120 Arc::new(FftOp::new(
2121 FftOperation::C2cForward,
2122 0,
2123 Some(2),
2124 FftNorm::Backward,
2125 )),
2126 &[input],
2127 )
2128 .unwrap()[0];
2129 let source = builder.finish(&[output]).unwrap();
2130 let ad = AdContext::builder()
2131 .with_semantic_extension_rules(semantic_ad_rules().unwrap())
2132 .unwrap()
2133 .build()
2134 .unwrap();
2135
2136 let jvp = ad.jvp_program(&source, &[true]).unwrap();
2137 assert_eq!(jvp.derivative_input_indices(), &[Some(1)]);
2138 assert!(matches!(
2139 jvp.frozen().program.operations().last().unwrap().op(),
2140 SemanticOpRef::Extension(op) if op.family_id() == FFT_EXTENSION_FAMILY_ID
2141 ));
2142
2143 let vjp = ad.vjp_program(&source, &[true], &[true]).unwrap();
2144 assert_eq!(vjp.derivative_output_indices(), &[Some(0)]);
2145 assert!(vjp.frozen().program.operations().any(|operation| matches!(
2146 operation.op(),
2147 SemanticOpRef::Core(CoreSemanticOp::PadToMatch { axis: 0 })
2148 | SemanticOpRef::Core(CoreSemanticOp::DynamicTruncate { axis: 0 })
2149 )));
2150 }
2151}