1use std::sync::Arc;
2
3use num_complex::{Complex32, Complex64};
4use tenferro_runtime::extension::apply;
5use tenferro_runtime::{CompareDir, DType, DotGeneralConfig, Error, Result, TracedTensor};
6
7use crate::extension::{
8 validate_derivative_eps, EighOptions, LinalgExtensionOp, LinalgOp, QrOptions, SvdOptions,
9};
10
11pub trait TracedTensorLinalgExt {
13 fn svd(&self) -> Result<(TracedTensor, TracedTensor, TracedTensor)>;
14 fn svd_with_options(
15 &self,
16 options: SvdOptions,
17 ) -> Result<(TracedTensor, TracedTensor, TracedTensor)>;
18 fn qr(&self) -> Result<(TracedTensor, TracedTensor)>;
19 fn qr_with_options(&self, options: QrOptions) -> Result<(TracedTensor, TracedTensor)>;
20 fn eigh(&self) -> Result<(TracedTensor, TracedTensor)>;
21 fn eigh_with_options(&self, options: EighOptions) -> Result<(TracedTensor, TracedTensor)>;
22 fn cholesky(&self) -> Result<TracedTensor>;
23 fn lu(&self) -> Result<(TracedTensor, TracedTensor, TracedTensor, TracedTensor)>;
24 fn full_piv_lu(
25 &self,
26 ) -> Result<(
27 TracedTensor,
28 TracedTensor,
29 TracedTensor,
30 TracedTensor,
31 TracedTensor,
32 )>;
33 fn eig(&self) -> Result<(TracedTensor, TracedTensor)>;
34 fn solve(&self, b: &TracedTensor) -> Result<TracedTensor>;
35 fn full_piv_lu_solve(&self, b: &TracedTensor) -> Result<TracedTensor>;
36 fn triangular_solve(
37 &self,
38 b: &TracedTensor,
39 left_side: bool,
40 lower: bool,
41 transpose_a: bool,
42 unit_diagonal: bool,
43 ) -> Result<TracedTensor>;
44 fn slogdet(&self) -> Result<(TracedTensor, TracedTensor)>;
45 fn det(&self) -> Result<TracedTensor>;
46 fn inv(&self) -> Result<TracedTensor>;
47 fn eigvalsh(&self) -> Result<TracedTensor>;
48 fn eigvals(&self) -> Result<TracedTensor>;
49 fn pinv(&self) -> Result<TracedTensor>;
50 fn pinv_with_rtol(&self, rtol: f64) -> Result<TracedTensor>;
51 fn norm(&self, ord: Option<f64>, dim: Option<&[usize]>, keepdim: bool) -> Result<TracedTensor>;
52}
53
54impl TracedTensorLinalgExt for TracedTensor {
55 fn svd(&self) -> Result<(TracedTensor, TracedTensor, TracedTensor)> {
56 svd(self)
57 }
58
59 fn svd_with_options(
60 &self,
61 options: SvdOptions,
62 ) -> Result<(TracedTensor, TracedTensor, TracedTensor)> {
63 svd_with_options(self, options)
64 }
65
66 fn qr(&self) -> Result<(TracedTensor, TracedTensor)> {
67 qr(self)
68 }
69
70 fn qr_with_options(&self, options: QrOptions) -> Result<(TracedTensor, TracedTensor)> {
71 qr_with_options(self, options)
72 }
73
74 fn eigh(&self) -> Result<(TracedTensor, TracedTensor)> {
75 eigh(self)
76 }
77
78 fn eigh_with_options(&self, options: EighOptions) -> Result<(TracedTensor, TracedTensor)> {
79 eigh_with_options(self, options)
80 }
81
82 fn cholesky(&self) -> Result<TracedTensor> {
83 cholesky(self)
84 }
85
86 fn lu(&self) -> Result<(TracedTensor, TracedTensor, TracedTensor, TracedTensor)> {
87 lu(self)
88 }
89
90 fn full_piv_lu(
91 &self,
92 ) -> Result<(
93 TracedTensor,
94 TracedTensor,
95 TracedTensor,
96 TracedTensor,
97 TracedTensor,
98 )> {
99 full_piv_lu(self)
100 }
101
102 fn eig(&self) -> Result<(TracedTensor, TracedTensor)> {
103 eig(self)
104 }
105
106 fn solve(&self, b: &TracedTensor) -> Result<TracedTensor> {
107 solve(self, b)
108 }
109
110 fn full_piv_lu_solve(&self, b: &TracedTensor) -> Result<TracedTensor> {
111 full_piv_lu_solve(self, b)
112 }
113
114 fn triangular_solve(
115 &self,
116 b: &TracedTensor,
117 left_side: bool,
118 lower: bool,
119 transpose_a: bool,
120 unit_diagonal: bool,
121 ) -> Result<TracedTensor> {
122 triangular_solve(self, b, left_side, lower, transpose_a, unit_diagonal)
123 }
124
125 fn slogdet(&self) -> Result<(TracedTensor, TracedTensor)> {
126 slogdet(self)
127 }
128
129 fn det(&self) -> Result<TracedTensor> {
130 det(self)
131 }
132
133 fn inv(&self) -> Result<TracedTensor> {
134 inv(self)
135 }
136
137 fn eigvalsh(&self) -> Result<TracedTensor> {
138 eigvalsh(self)
139 }
140
141 fn eigvals(&self) -> Result<TracedTensor> {
142 eigvals(self)
143 }
144
145 fn pinv(&self) -> Result<TracedTensor> {
146 pinv(self)
147 }
148
149 fn pinv_with_rtol(&self, rtol: f64) -> Result<TracedTensor> {
150 pinv_with_rtol(self, rtol)
151 }
152
153 fn norm(&self, ord: Option<f64>, dim: Option<&[usize]>, keepdim: bool) -> Result<TracedTensor> {
154 norm(self, ord, dim, keepdim)
155 }
156}
157
158pub fn svd(a: &TracedTensor) -> Result<(TracedTensor, TracedTensor, TracedTensor)> {
173 svd_with_options(a, SvdOptions::default())
174}
175
176pub fn svd_with_options(
195 a: &TracedTensor,
196 options: SvdOptions,
197) -> Result<(TracedTensor, TracedTensor, TracedTensor)> {
198 validate_derivative_eps("svd_with_options", options.derivative_eps)?;
199 three_outputs(
200 apply(
201 Arc::new(LinalgExtensionOp::new(LinalgOp::Svd {
202 derivative_eps: options.derivative_eps,
203 gauge: options.gauge,
204 })),
205 &[a],
206 )?,
207 "svd",
208 )
209}
210
211pub fn qr(a: &TracedTensor) -> Result<(TracedTensor, TracedTensor)> {
225 qr_with_options(a, QrOptions::default())
226}
227
228pub fn qr_with_options(
244 a: &TracedTensor,
245 options: QrOptions,
246) -> Result<(TracedTensor, TracedTensor)> {
247 two_outputs(
248 apply(
249 Arc::new(LinalgExtensionOp::new(LinalgOp::Qr {
250 gauge: options.gauge,
251 })),
252 &[a],
253 )?,
254 "qr",
255 )
256}
257
258pub fn eigh(a: &TracedTensor) -> Result<(TracedTensor, TracedTensor)> {
272 eigh_with_options(a, EighOptions::default())
273}
274
275pub fn eigh_with_options(
297 a: &TracedTensor,
298 options: EighOptions,
299) -> Result<(TracedTensor, TracedTensor)> {
300 validate_derivative_eps("eigh_with_options", options.derivative_eps)?;
301 two_outputs(
302 apply(
303 Arc::new(LinalgExtensionOp::new(LinalgOp::Eigh {
304 derivative_eps: options.derivative_eps,
305 gauge: options.gauge,
306 })),
307 &[a],
308 )?,
309 "eigh",
310 )
311}
312
313pub fn cholesky(a: &TracedTensor) -> Result<TracedTensor> {
326 one_output(
327 apply(Arc::new(LinalgExtensionOp::new(LinalgOp::Cholesky)), &[a])?,
328 "cholesky",
329 )
330}
331
332pub fn lu(a: &TracedTensor) -> Result<(TracedTensor, TracedTensor, TracedTensor, TracedTensor)> {
348 four_outputs(
349 apply(Arc::new(LinalgExtensionOp::new(LinalgOp::Lu)), &[a])?,
350 "lu",
351 )
352}
353
354pub fn full_piv_lu(
376 a: &TracedTensor,
377) -> Result<(
378 TracedTensor,
379 TracedTensor,
380 TracedTensor,
381 TracedTensor,
382 TracedTensor,
383)> {
384 five_outputs(
385 apply(Arc::new(LinalgExtensionOp::new(LinalgOp::FullPivLu)), &[a])?,
386 "full_piv_lu",
387 )
388}
389
390pub fn eig(a: &TracedTensor) -> Result<(TracedTensor, TracedTensor)> {
404 two_outputs(
405 apply(
406 Arc::new(LinalgExtensionOp::new(LinalgOp::Eig {
407 input_dtype: a.dtype,
408 })),
409 &[a],
410 )?,
411 "eig",
412 )
413}
414
415pub fn solve(a: &TracedTensor, b: &TracedTensor) -> Result<TracedTensor> {
429 let mut factor_outputs =
430 apply(Arc::new(LinalgExtensionOp::new(LinalgOp::LuFactor)), &[a])?.into_iter();
431 let (packed_lu, pivots) = match (
432 factor_outputs.next(),
433 factor_outputs.next(),
434 factor_outputs.next(),
435 factor_outputs.next(),
436 ) {
437 (Some(packed_lu), Some(pivots), Some(_parity), None) => (packed_lu, pivots),
438 _ => return Err(unexpected_output_count("lu_factor", 3)),
439 };
440 one_output(
441 apply(
442 Arc::new(LinalgExtensionOp::new(LinalgOp::LuSolvePrepared {
443 transpose_a: false,
444 conjugate_a: false,
445 })),
446 &[a, &packed_lu, &pivots, b],
447 )?,
448 "solve",
449 )
450}
451
452pub fn full_piv_lu_solve(a: &TracedTensor, b: &TracedTensor) -> Result<TracedTensor> {
466 one_output(
467 apply(
468 Arc::new(LinalgExtensionOp::new(LinalgOp::FullPivLuSolve {
469 transpose_a: false,
470 })),
471 &[a, b],
472 )?,
473 "full_piv_lu_solve",
474 )
475}
476
477pub fn triangular_solve(
491 a: &TracedTensor,
492 b: &TracedTensor,
493 left_side: bool,
494 lower: bool,
495 transpose_a: bool,
496 unit_diagonal: bool,
497) -> Result<TracedTensor> {
498 one_output(
499 apply(
500 Arc::new(LinalgExtensionOp::new(LinalgOp::TriangularSolve {
501 left_side,
502 lower,
503 transpose_a,
504 unit_diagonal,
505 })),
506 &[a, b],
507 )?,
508 "triangular_solve",
509 )
510}
511
512pub fn slogdet(a: &TracedTensor) -> Result<(TracedTensor, TracedTensor)> {
526 let mut factor_outputs =
527 apply(Arc::new(LinalgExtensionOp::new(LinalgOp::LuFactor)), &[a])?.into_iter();
528 let (packed_lu, parity) = match (
529 factor_outputs.next(),
530 factor_outputs.next(),
531 factor_outputs.next(),
532 factor_outputs.next(),
533 ) {
534 (Some(packed_lu), Some(_pivots), Some(parity), None) => (packed_lu, parity),
535 _ => return Err(unexpected_output_count("lu_factor", 3)),
536 };
537 let diag_u = packed_lu.extract_diag(0, 1)?;
538 let sign_u = diag_u.sign()?.reduce_prod(&[0])?;
539 let sign = (&parity * &sign_u)?;
540 let logabsdet = diag_u.abs()?.log()?.reduce_sum(&[0])?;
541 Ok((sign, logabsdet))
542}
543
544pub fn det(a: &TracedTensor) -> Result<TracedTensor> {
557 let (sign, logabsdet) = slogdet(a)?;
558 &sign * &logabsdet.exp()?
559}
560
561pub fn inv(a: &TracedTensor) -> Result<TracedTensor> {
574 ensure_min_rank("inv", a.rank, 2)?;
575 let shape = require_concrete_shape("inv", a)?;
576 let eye = eye_like(a, shape[0])?;
577 solve(a, &eye)
578}
579
580pub fn eigvalsh(a: &TracedTensor) -> Result<TracedTensor> {
593 eigh_values(a)
594}
595
596pub fn eigvals(a: &TracedTensor) -> Result<TracedTensor> {
609 eig_values(a)
610}
611
612pub fn pinv(a: &TracedTensor) -> Result<TracedTensor> {
628 ensure_float_or_complex("pinv", a.dtype)?;
629 let shape = require_concrete_shape("pinv", a)?;
630 let max_dim = match (shape.first(), shape.get(1)) {
631 (Some(&m), Some(&n)) => m.max(n),
632 (Some(&m), None) => m,
633 _ => 0,
634 };
635 pinv_with_rtol(a, default_pinv_rtol(a.dtype, max_dim))
636}
637
638pub fn pinv_with_rtol(a: &TracedTensor, rtol: f64) -> Result<TracedTensor> {
654 ensure_float_or_complex("pinv_with_rtol", a.dtype)?;
655 require_concrete_shape("pinv_with_rtol", a)?;
656 let (u, s, vt) = svd(a)?;
657 let abs_s = s.abs()?;
658 let s_max = abs_s.reduce_max(&[0])?;
659 let s_max_shape = s_max.concrete_shape()?;
660 let threshold_scalar = broadcast_scalar(scalar_real(s.dtype, rtol.max(0.0))?, &s_max_shape)?;
661 let threshold = (&s_max * &threshold_scalar)?;
662 let s_shape = s.concrete_shape()?;
663 let threshold = broadcast_batch_scalar_to_leading_axis(&threshold, &s_shape)?;
664 let mask = abs_s.compare(&threshold, CompareDir::Gt)?;
665 let mask = mask.convert(s.dtype)?;
666 let ones = ones_like(&s)?;
667 let neg_mask = (-&mask)?;
668 let denom = (&s + &(&ones + &neg_mask)?)?;
669 let s_inv = (&mask / &denom)?;
670
671 let v = vt.conj()?.transpose(&matrix_transpose_perm(vt.rank))?;
672 let uh = u.conj()?.transpose(&matrix_transpose_perm(u.rank))?;
673 let vs = scale_matrix_columns(&v, &s_inv)?;
674 matmul_preserve_trailing_batch(&vs, &uh)
675}
676
677pub fn norm(
693 a: &TracedTensor,
694 ord: Option<f64>,
695 dim: Option<&[usize]>,
696 keepdim: bool,
697) -> Result<TracedTensor> {
698 ensure_float_or_complex("norm", a.dtype)?;
699 let shape = require_concrete_shape("norm", a)?;
700 let axes = dim.map_or_else(|| (0..a.rank).collect::<Vec<_>>(), |dims| dims.to_vec());
701 if axes.is_empty() {
702 return Ok(a.clone());
703 }
704 validate_axes("norm", a.rank, &axes)?;
705
706 let out = match axes.len() {
707 1 => vector_norm(a, axes[0], ord)?,
708 2 => matrix_norm(a, &axes, ord)?,
709 _ => {
710 let abs = a.abs()?;
711 match ord {
712 None => frobenius_norm(&abs, &axes)?,
713 Some(p) if p == f64::INFINITY => abs.reduce_max(&axes)?,
714 Some(p) if p == f64::NEG_INFINITY => abs.reduce_min(&axes)?,
715 Some(0.0) => count_nonzero(&abs, &axes)?,
716 Some(p) => p_norm(&abs, &axes, p)?,
717 }
718 }
719 };
720 restore_keepdim(out, &shape, &axes, keepdim)
721}
722
723fn unexpected_output_count(name: &str, expected: usize) -> Error {
724 Error::Internal(format!("{name} must produce exactly {expected} outputs"))
725}
726
727fn one_output(outputs: Vec<TracedTensor>, name: &str) -> Result<TracedTensor> {
728 let mut outputs = outputs.into_iter();
729 match (outputs.next(), outputs.next()) {
730 (Some(output), None) => Ok(output),
731 _ => Err(unexpected_output_count(name, 1)),
732 }
733}
734
735fn two_outputs(outputs: Vec<TracedTensor>, name: &str) -> Result<(TracedTensor, TracedTensor)> {
736 let mut outputs = outputs.into_iter();
737 match (outputs.next(), outputs.next(), outputs.next()) {
738 (Some(lhs), Some(rhs), None) => Ok((lhs, rhs)),
739 _ => Err(unexpected_output_count(name, 2)),
740 }
741}
742
743fn three_outputs(
744 outputs: Vec<TracedTensor>,
745 name: &str,
746) -> Result<(TracedTensor, TracedTensor, TracedTensor)> {
747 let mut outputs = outputs.into_iter();
748 match (
749 outputs.next(),
750 outputs.next(),
751 outputs.next(),
752 outputs.next(),
753 ) {
754 (Some(first), Some(second), Some(third), None) => Ok((first, second, third)),
755 _ => Err(unexpected_output_count(name, 3)),
756 }
757}
758
759fn four_outputs(
760 outputs: Vec<TracedTensor>,
761 name: &str,
762) -> Result<(TracedTensor, TracedTensor, TracedTensor, TracedTensor)> {
763 let mut outputs = outputs.into_iter();
764 match (
765 outputs.next(),
766 outputs.next(),
767 outputs.next(),
768 outputs.next(),
769 outputs.next(),
770 ) {
771 (Some(first), Some(second), Some(third), Some(fourth), None) => {
772 Ok((first, second, third, fourth))
773 }
774 _ => Err(unexpected_output_count(name, 4)),
775 }
776}
777
778fn five_outputs(
779 outputs: Vec<TracedTensor>,
780 name: &str,
781) -> Result<(
782 TracedTensor,
783 TracedTensor,
784 TracedTensor,
785 TracedTensor,
786 TracedTensor,
787)> {
788 let mut outputs = outputs.into_iter();
789 match (
790 outputs.next(),
791 outputs.next(),
792 outputs.next(),
793 outputs.next(),
794 outputs.next(),
795 outputs.next(),
796 ) {
797 (Some(first), Some(second), Some(third), Some(fourth), Some(fifth), None) => {
798 Ok((first, second, third, fourth, fifth))
799 }
800 _ => Err(unexpected_output_count(name, 5)),
801 }
802}
803
804fn scalar_real(dtype: DType, value: f64) -> Result<TracedTensor> {
805 match dtype {
806 DType::F64 => TracedTensor::from_vec_col_major(vec![], vec![value]),
807 DType::F32 => TracedTensor::from_vec_col_major(vec![], vec![value as f32]),
808 DType::I32 => TracedTensor::from_vec_col_major(vec![], vec![value.round() as i32]),
809 DType::I64 => TracedTensor::from_vec_col_major(vec![], vec![value.round() as i64]),
810 DType::Bool => TracedTensor::from_vec_col_major(vec![], vec![value != 0.0]),
811 DType::C64 => TracedTensor::from_vec_col_major(vec![], vec![Complex64::new(value, 0.0)]),
812 DType::C32 => {
813 TracedTensor::from_vec_col_major(vec![], vec![Complex32::new(value as f32, 0.0)])
814 }
815 }
816}
817
818fn ensure_float_or_complex(op: &'static str, dtype: DType) -> Result<()> {
819 match dtype {
820 DType::F32 | DType::F64 | DType::C32 | DType::C64 => Ok(()),
821 DType::I32 | DType::I64 | DType::Bool => Err(Error::TensorRuntime(
822 tenferro_tensor::Error::backend_failure(op, format!("unsupported dtype {dtype:?}")),
823 )),
824 }
825}
826
827fn ensure_min_rank(op: &'static str, actual: usize, expected: usize) -> Result<()> {
828 if actual < expected {
829 return Err(Error::TensorRuntime(tenferro_tensor::Error::RankMismatch {
830 op,
831 expected,
832 actual,
833 }));
834 }
835 Ok(())
836}
837
838fn validate_axes(op: &'static str, rank: usize, axes: &[usize]) -> Result<()> {
839 for &axis in axes {
840 if axis >= rank {
841 return Err(Error::TensorRuntime(
842 tenferro_tensor::Error::AxisOutOfBounds { op, axis, rank },
843 ));
844 }
845 }
846 Ok(())
847}
848
849fn require_concrete_shape(op: &'static str, input: &TracedTensor) -> Result<Vec<usize>> {
850 input.try_concrete_shape().ok_or_else(|| {
851 Error::TensorRuntime(tenferro_tensor::Error::backend_failure(
852 op,
853 "symbolic shape is not supported by this traced linalg helper",
854 ))
855 })
856}
857
858fn zero_scalar(dtype: DType) -> Result<TracedTensor> {
859 scalar_real(dtype, 0.0)
860}
861
862fn one_scalar(dtype: DType) -> Result<TracedTensor> {
863 scalar_real(dtype, 1.0)
864}
865
866fn ones_like(input: &TracedTensor) -> Result<TracedTensor> {
867 let shape = input.concrete_shape()?;
868 broadcast_scalar(one_scalar(input.dtype)?, &shape)
869}
870
871fn eye_like(anchor: &TracedTensor, size: usize) -> Result<TracedTensor> {
872 let mut vector_shape = vec![size];
873 let anchor_shape = anchor.concrete_shape()?;
874 vector_shape.extend_from_slice(&anchor_shape[2..]);
875 let diagonal = broadcast_scalar(one_scalar(anchor.dtype)?, &vector_shape)?;
876 diagonal.embed_diag(0, 1)
877}
878
879fn broadcast_scalar(input: TracedTensor, shape: &[usize]) -> Result<TracedTensor> {
880 let input_shape = input.concrete_shape()?;
881 if input_shape == shape {
882 return Ok(input);
883 }
884 input.broadcast_in_dim(shape, &[])
885}
886
887fn broadcast_batch_scalar_to_leading_axis(
888 input: &TracedTensor,
889 shape: &[usize],
890) -> Result<TracedTensor> {
891 let input_shape = input.concrete_shape()?;
892 if input_shape == shape {
893 return Ok(input.clone());
894 }
895 let dims: Vec<usize> = (1..shape.len()).collect();
896 input.broadcast_in_dim(shape, &dims)
897}
898
899fn matmul_preserve_trailing_batch(lhs: &TracedTensor, rhs: &TracedTensor) -> Result<TracedTensor> {
900 let rank = lhs.rank;
901 let batch_dims: Vec<usize> = (2..rank).collect();
902 lhs.dot_general(
903 rhs,
904 DotGeneralConfig {
905 lhs_contracting_dims: vec![1],
906 rhs_contracting_dims: vec![0],
907 lhs_batch_dims: batch_dims.clone(),
908 rhs_batch_dims: batch_dims,
909 },
910 )
911}
912
913fn matrix_transpose_perm(rank: usize) -> Vec<usize> {
914 let mut perm: Vec<usize> = (0..rank).collect();
915 perm.swap(0, 1);
916 perm
917}
918
919fn frobenius_norm(abs: &TracedTensor, axes: &[usize]) -> Result<TracedTensor> {
920 let squared = abs.pow(&scalar_real(abs.dtype, 2.0)?)?;
921 squared.reduce_sum(axes)?.sqrt()
922}
923
924fn p_norm(abs: &TracedTensor, axes: &[usize], p: f64) -> Result<TracedTensor> {
925 if !p.is_finite() || p == 0.0 {
926 return Err(Error::InvalidGraphBuild {
927 op: "norm",
928 message: format!("p-norm order must be finite and nonzero, got {p}"),
929 });
930 }
931 let power = abs.pow(&scalar_real(abs.dtype, p)?)?;
932 let inv_p = scalar_real(abs.dtype, 1.0 / p)?;
933 power.reduce_sum(axes)?.pow(&inv_p)
934}
935
936fn default_pinv_rtol(dtype: DType, max_dim: usize) -> f64 {
937 let eps = match dtype {
938 DType::F32 | DType::C32 => f32::EPSILON as f64,
939 DType::F64 | DType::C64 => f64::EPSILON,
940 DType::I32 | DType::I64 | DType::Bool => 0.0,
941 };
942 eps * max_dim as f64
943}
944
945fn vector_norm(a: &TracedTensor, axis: usize, ord: Option<f64>) -> Result<TracedTensor> {
946 let abs = a.abs()?;
947 match ord {
948 None => frobenius_norm(&abs, &[axis]),
949 Some(0.0) => count_nonzero(&abs, &[axis]),
950 Some(p) if p == f64::INFINITY => abs.reduce_max(&[axis]),
951 Some(p) if p == f64::NEG_INFINITY => abs.reduce_min(&[axis]),
952 Some(p) => p_norm(&abs, &[axis], p),
953 }
954}
955
956fn matrix_norm(a: &TracedTensor, axes: &[usize], ord: Option<f64>) -> Result<TracedTensor> {
957 let matrix = move_axes_to_front(a, axes)?;
958 let abs = matrix.abs()?;
959 match ord {
960 None => frobenius_norm(&abs, &[0, 1]),
961 Some(p) if p == f64::INFINITY => matrix_row_sum_norm(&abs, true),
962 Some(p) if p == f64::NEG_INFINITY => matrix_row_sum_norm(&abs, false),
963 Some(1.0) => matrix_col_sum_norm(&abs, true),
964 Some(-1.0) => matrix_col_sum_norm(&abs, false),
965 Some(2.0) => {
966 let singular_values = svd_values(&matrix)?.abs()?;
967 singular_values.reduce_max(&[0])
968 }
969 Some(-2.0) => {
970 let singular_values = svd_values(&matrix)?.abs()?;
971 singular_values.reduce_min(&[0])
972 }
973 Some(0.0) => count_nonzero(&abs, &[0, 1]),
974 Some(p) => p_norm(&abs, &[0, 1], p),
975 }
976}
977
978fn svd_values(a: &TracedTensor) -> Result<TracedTensor> {
979 let (_u, s, _vt) = three_outputs(
980 apply(
981 Arc::new(LinalgExtensionOp::new(LinalgOp::Svd {
982 derivative_eps: SvdOptions::default().derivative_eps,
983 gauge: SvdOptions::default().gauge,
984 })),
985 &[a],
986 )?,
987 "svd_values",
988 )?;
989 Ok(s)
990}
991
992fn eigh_values(a: &TracedTensor) -> Result<TracedTensor> {
993 let (values, _vectors) = two_outputs(
994 apply(
995 Arc::new(LinalgExtensionOp::new(LinalgOp::Eigh {
996 derivative_eps: EighOptions::default().derivative_eps,
997 gauge: EighOptions::default().gauge,
998 })),
999 &[a],
1000 )?,
1001 "eigh_values",
1002 )?;
1003 Ok(values)
1004}
1005
1006fn eig_values(a: &TracedTensor) -> Result<TracedTensor> {
1007 let (values, _vectors) = two_outputs(
1008 apply(
1009 Arc::new(LinalgExtensionOp::new(LinalgOp::Eig {
1010 input_dtype: a.dtype,
1011 })),
1012 &[a],
1013 )?,
1014 "eig_values",
1015 )?;
1016 Ok(values)
1017}
1018
1019fn scale_matrix_columns(matrix: &TracedTensor, scale: &TracedTensor) -> Result<TracedTensor> {
1020 let matrix_shape = matrix.concrete_shape()?;
1021 let scale_shape_input = scale.concrete_shape()?;
1022 let mut scale_shape = vec![1, scale_shape_input[0]];
1023 scale_shape.extend_from_slice(&matrix_shape[2..]);
1024 let dims: Vec<usize> = (0..matrix_shape.len()).collect();
1025 let scale = scale
1026 .reshape(&scale_shape)?
1027 .broadcast_in_dim(&matrix_shape, &dims)?;
1028 matrix * &scale
1029}
1030
1031fn count_nonzero(abs: &TracedTensor, axes: &[usize]) -> Result<TracedTensor> {
1032 let mask = abs.compare(&zero_scalar(abs.dtype)?, CompareDir::Gt)?;
1033 mask.convert(abs.dtype)?.reduce_sum(axes)
1034}
1035
1036fn matrix_row_sum_norm(abs: &TracedTensor, take_max: bool) -> Result<TracedTensor> {
1037 let row_sums = abs.reduce_sum(&[1])?;
1038 if take_max {
1039 row_sums.reduce_max(&[0])
1040 } else {
1041 row_sums.reduce_min(&[0])
1042 }
1043}
1044
1045fn matrix_col_sum_norm(abs: &TracedTensor, take_max: bool) -> Result<TracedTensor> {
1046 let col_sums = abs.reduce_sum(&[0])?;
1047 if take_max {
1048 col_sums.reduce_max(&[0])
1049 } else {
1050 col_sums.reduce_min(&[0])
1051 }
1052}
1053
1054fn move_axes_to_front(tensor: &TracedTensor, axes: &[usize]) -> Result<TracedTensor> {
1055 if axes.iter().enumerate().all(|(index, &axis)| index == axis) {
1056 return Ok(tensor.clone());
1057 }
1058
1059 let mut selected = vec![false; tensor.rank];
1060 for &axis in axes {
1061 selected[axis] = true;
1062 }
1063
1064 let mut perm = Vec::with_capacity(tensor.rank);
1065 perm.extend_from_slice(axes);
1066 for (axis, is_selected) in selected.iter().enumerate().take(tensor.rank) {
1067 if !*is_selected {
1068 perm.push(axis);
1069 }
1070 }
1071 tensor.transpose(&perm)
1072}
1073
1074fn restore_keepdim(
1075 reduced: TracedTensor,
1076 original_shape: &[usize],
1077 axes: &[usize],
1078 keepdim: bool,
1079) -> Result<TracedTensor> {
1080 if !keepdim {
1081 return Ok(reduced);
1082 }
1083 let mut kept_shape = original_shape.to_vec();
1084 for &axis in axes {
1085 kept_shape[axis] = 1;
1086 }
1087 reduced.reshape(&kept_shape)
1088}
1089
1090#[cfg(test)]
1091mod tests {
1092 use super::p_norm;
1093 use tenferro_runtime::TracedTensor;
1094
1095 #[test]
1096 fn p_norm_rejects_zero_and_non_finite_orders() {
1097 let x = TracedTensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap();
1098 let abs = x.abs().unwrap();
1099
1100 for p in [0.0, f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
1101 let err = p_norm(&abs, &[0], p).unwrap_err();
1102 assert!(
1103 err.to_string().contains("finite") || err.to_string().contains("nonzero"),
1104 "expected finite nonzero order error, got {err:?}"
1105 );
1106 }
1107 }
1108}