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