Skip to main content

tenferro_linalg/
traced.rs

1use std::sync::Arc;
2
3use num_complex::{Complex32, Complex64};
4use tenferro_runtime::extension::apply;
5use tenferro_runtime::{
6    CompareDir, DType, DotGeneralConfig, Error, ErrorPhase, Result, TracedTensor,
7};
8
9use crate::extension::{
10    validate_derivative_eps, EighOptions, LinalgExtensionOp, LinalgOp, QrOptions, SvdOptions,
11};
12use crate::validation::{ensure_float_or_complex, validate_lstsq};
13
14/// Linear algebra extension methods for [`TracedTensor`].
15pub trait TracedTensorLinalgExt {
16    /// Build a traced SVD operation with default options.
17    ///
18    /// # Errors
19    ///
20    /// Returns `Error::Extension` with `ErrorKind::Unsupported` for an
21    /// unsupported dtype, or `Error::Validation` for invalid graph metadata.
22    ///
23    /// # Deferred errors
24    ///
25    /// Backend numerical failures and concrete shape mismatches can be
26    /// reported as `Error::Extension` or `Error::Validation` during compile or
27    /// execution when symbolic inputs are bound.
28    fn svd(&self) -> Result<(TracedTensor, TracedTensor, TracedTensor)>;
29
30    /// Build a traced SVD operation with explicit derivative and gauge options.
31    ///
32    /// # Errors
33    ///
34    /// Returns `Error::Validation::InvalidArgument` for a non-finite or
35    /// non-positive derivative epsilon, or `Error::Extension` for unsupported
36    /// dtype and graph registration failures.
37    ///
38    /// # Deferred errors
39    ///
40    /// Solver convergence and symbolic shape checks may be reported during
41    /// compile or execution.
42    fn svd_with_options(
43        &self,
44        options: SvdOptions,
45    ) -> Result<(TracedTensor, TracedTensor, TracedTensor)>;
46
47    /// Build a traced full-matrices SVD operation returning square `U (m x m)`
48    /// and `Vh (n x n)`, whose trailing `n - rank` rows span the input's right
49    /// nullspace.
50    ///
51    /// # Errors
52    ///
53    /// Returns `Error::Validation` when the input is not a batched matrix
54    /// (rank `>= 2`), `Error::Extension` with `ErrorKind::Unsupported` for
55    /// integer or boolean dtypes, or `Error::Extension` for graph
56    /// registration failures.
57    ///
58    /// # Deferred errors
59    ///
60    /// The active backend returns `Error::Extension` with
61    /// `ErrorKind::Unsupported` at execution if it does not implement
62    /// full-matrices SVD (only the CPU faer provider does in this slice; the
63    /// LAPACK provider and GPU backends are unsupported). Automatic
64    /// differentiation is intentionally unsupported for the full variant (see
65    /// the linalg AD support manifest) and surfaces a typed AD error rather
66    /// than a silent thin-SVD fallback.
67    fn svd_full(&self) -> Result<(TracedTensor, TracedTensor, TracedTensor)>;
68
69    /// Build a traced QR operation.
70    ///
71    /// # Errors
72    ///
73    /// Returns `Error::Extension` with `ErrorKind::Unsupported` for an
74    /// unsupported dtype or `Error::Validation` for invalid graph metadata.
75    ///
76    /// # Deferred errors
77    ///
78    /// Concrete shape validation and backend QR failures may be reported at
79    /// compile or execution time for symbolic inputs.
80    fn qr(&self) -> Result<(TracedTensor, TracedTensor)>;
81
82    /// Build a traced QR operation with explicit gauge options.
83    ///
84    /// # Errors
85    ///
86    /// Returns `Error::Extension` with `ErrorKind::Unsupported` for an
87    /// unsupported dtype, or `Error::Validation` for invalid graph metadata.
88    ///
89    /// # Deferred errors
90    ///
91    /// Symbolic shape checks and backend QR failures can be deferred to compile
92    /// or execution.
93    fn qr_with_options(&self, options: QrOptions) -> Result<(TracedTensor, TracedTensor)>;
94
95    /// Build a traced Hermitian eigendecomposition operation.
96    ///
97    /// # Errors
98    ///
99    /// Returns `Error::Extension` with `ErrorKind::Unsupported` for an
100    /// unsupported dtype or `Error::Validation` for invalid graph metadata.
101    ///
102    /// # Deferred errors
103    ///
104    /// Concrete square-shape validation and solver failures may be reported at
105    /// compile or execution time.
106    fn eigh(&self) -> Result<(TracedTensor, TracedTensor)>;
107
108    /// Build a traced Hermitian eigendecomposition with explicit options.
109    ///
110    /// # Errors
111    ///
112    /// Returns `Error::Validation::InvalidArgument` for an invalid derivative
113    /// epsilon, or `Error::Extension` for unsupported dtype and registration
114    /// failures.
115    ///
116    /// # Deferred errors
117    ///
118    /// Symbolic square-shape checks and numerical eigensolver failures may be
119    /// reported during compile or execution.
120    fn eigh_with_options(&self, options: EighOptions) -> Result<(TracedTensor, TracedTensor)>;
121
122    /// Build a traced Cholesky factorization operation.
123    ///
124    /// # Errors
125    ///
126    /// Returns `Error::Extension` with `ErrorKind::Unsupported` for an
127    /// unsupported dtype or `Error::Validation` for invalid graph metadata.
128    ///
129    /// # Deferred errors
130    ///
131    /// Non-square or non-positive-definite concrete inputs can produce
132    /// validation or numerical extension errors during compile or execution.
133    fn cholesky(&self) -> Result<TracedTensor>;
134
135    /// Build a traced LU factorization operation.
136    ///
137    /// # Errors
138    ///
139    /// Returns `Error::Extension` with `ErrorKind::Unsupported` for an
140    /// unsupported dtype or `Error::Validation` for invalid graph metadata.
141    ///
142    /// # Deferred errors
143    ///
144    /// Concrete shape checks and backend factorization failures may be
145    /// reported during compile or execution.
146    fn lu(&self) -> Result<(TracedTensor, TracedTensor, TracedTensor, TracedTensor)>;
147
148    /// Build a traced complete-pivot LU factorization operation.
149    ///
150    /// # Errors
151    ///
152    /// Returns `Error::Extension` with `ErrorKind::Unsupported` for an
153    /// unsupported dtype or `Error::Validation` for invalid graph metadata.
154    ///
155    /// # Deferred errors
156    ///
157    /// Concrete square-shape checks and backend factorization failures may be
158    /// reported during compile or execution.
159    fn full_piv_lu(
160        &self,
161    ) -> Result<(
162        TracedTensor,
163        TracedTensor,
164        TracedTensor,
165        TracedTensor,
166        TracedTensor,
167    )>;
168    /// Build a traced general eigendecomposition operation.
169    ///
170    /// # Errors
171    ///
172    /// Returns `Error::Extension` with `ErrorKind::Unsupported` for an
173    /// unsupported dtype or `Error::Validation` for invalid graph metadata.
174    ///
175    /// # Deferred errors
176    ///
177    /// Concrete shape validation and numerical eigensolver failures may be
178    /// reported during compile or execution.
179    fn eig(&self) -> Result<(TracedTensor, TracedTensor)>;
180
181    /// Build a traced linear solve operation.
182    ///
183    /// # Errors
184    ///
185    /// Returns `Error::Validation` for incompatible coefficient/rhs metadata
186    /// and `Error::Extension` for unsupported dtype or registration failures.
187    ///
188    /// # Deferred errors
189    ///
190    /// Singular systems and concrete shape mismatches are reported as
191    /// numerical or validation errors during compile or execution.
192    fn solve(&self, b: &TracedTensor) -> Result<TracedTensor>;
193
194    /// Build a traced least-squares solve `argmin_x ||A x - b||_2` for a tall
195    /// or square, full-column-rank `A`, via the thin QR factorization.
196    ///
197    /// # Errors
198    ///
199    /// Returns `Error::Validation` for an invalid rank (`A` or `b` not a
200    /// batched matrix, rank `< 2`), a symbolic shape, a wide/underdetermined
201    /// `A` (`rows < cols`), or an unsupported dtype (not floating-point or
202    /// complex).
203    ///
204    /// # Deferred errors
205    ///
206    /// Backend QR and triangular-solve failures and concrete shape mismatches
207    /// are reported during compile or execution. Rank-deficient `A` is not
208    /// detected: `R` is singular and the result is ill-defined, so callers must
209    /// ensure full column rank.
210    fn lstsq(&self, b: &TracedTensor) -> Result<TracedTensor>;
211
212    /// Build a traced complete-pivot LU solve operation.
213    ///
214    /// # Errors
215    ///
216    /// Returns `Error::Validation` for incompatible coefficient/rhs metadata
217    /// and `Error::Extension` for unsupported dtype or registration failures.
218    ///
219    /// # Deferred errors
220    ///
221    /// Singular systems and concrete shape mismatches may be reported during
222    /// compile or execution.
223    fn full_piv_lu_solve(&self, b: &TracedTensor) -> Result<TracedTensor>;
224
225    /// Build a traced triangular solve operation.
226    ///
227    /// # Errors
228    ///
229    /// Returns `Error::Validation` for incompatible coefficient/rhs shapes or
230    /// invalid solve flags, and `Error::Extension` for unsupported dtype.
231    ///
232    /// # Deferred errors
233    ///
234    /// Singular or zero-diagonal systems can fail numerically during compile or
235    /// execution after symbolic inputs are bound.
236    fn triangular_solve(
237        &self,
238        b: &TracedTensor,
239        left_side: bool,
240        lower: bool,
241        transpose_a: bool,
242        unit_diagonal: bool,
243    ) -> Result<TracedTensor>;
244    /// Build a traced sign/log-determinant operation.
245    ///
246    /// # Errors
247    ///
248    /// Returns `Error::Validation` for invalid matrix metadata or
249    /// `Error::Extension` for unsupported dtype and registration failures.
250    ///
251    /// # Deferred errors
252    ///
253    /// Concrete singularity and shape failures can be reported during compile
254    /// or execution.
255    fn slogdet(&self) -> Result<(TracedTensor, TracedTensor)>;
256
257    /// Build a traced determinant operation.
258    ///
259    /// # Errors
260    ///
261    /// Returns `Error::Validation` for invalid matrix metadata or
262    /// `Error::Extension` for unsupported dtype.
263    ///
264    /// # Deferred errors
265    ///
266    /// Concrete singularity and shape failures may be reported during compile
267    /// or execution.
268    fn det(&self) -> Result<TracedTensor>;
269
270    /// Build a traced matrix-inverse operation.
271    ///
272    /// # Errors
273    ///
274    /// Returns `Error::Validation` for incompatible rank/shape metadata or
275    /// `Error::Extension` for unsupported dtype.
276    ///
277    /// # Deferred errors
278    ///
279    /// Singular matrices produce a numerical error during compile or execution.
280    fn inv(&self) -> Result<TracedTensor>;
281
282    /// Build a traced Hermitian eigenvalue-only operation.
283    ///
284    /// # Errors
285    ///
286    /// Returns `Error::Validation` for non-square metadata or
287    /// `Error::Extension` for unsupported dtype.
288    ///
289    /// # Deferred errors
290    ///
291    /// Concrete square-shape and solver failures may be reported during compile
292    /// or execution.
293    fn eigvalsh(&self) -> Result<TracedTensor>;
294
295    /// Build a traced general eigenvalue-only operation.
296    ///
297    /// # Errors
298    ///
299    /// Returns `Error::Validation` for invalid matrix metadata or
300    /// `Error::Extension` for unsupported dtype.
301    ///
302    /// # Deferred errors
303    ///
304    /// Concrete shape and eigensolver failures may be reported during compile
305    /// or execution.
306    fn eigvals(&self) -> Result<TracedTensor>;
307
308    /// Build a traced pseudoinverse operation with the default tolerance.
309    ///
310    /// # Errors
311    ///
312    /// Returns `Error::Validation` for invalid rank/shape metadata or
313    /// `Error::Extension` for unsupported dtype.
314    ///
315    /// # Deferred errors
316    ///
317    /// SVD convergence and concrete shape failures may be reported during
318    /// compile or execution.
319    fn pinv(&self) -> Result<TracedTensor>;
320
321    /// Build a traced pseudoinverse with an explicit relative tolerance.
322    ///
323    /// # Errors
324    ///
325    /// Returns `Error::Validation::InvalidArgument` when `rtol` is non-finite
326    /// or negative, or `Error::Extension` for unsupported dtype.
327    ///
328    /// # Deferred errors
329    ///
330    /// SVD convergence and concrete shape failures may be reported during
331    /// compile or execution.
332    fn pinv_with_rtol(&self, rtol: f64) -> Result<TracedTensor>;
333
334    /// Build a traced vector/matrix norm operation.
335    ///
336    /// # Errors
337    ///
338    /// Returns `Error::Validation` for an invalid norm order or axis and
339    /// `Error::Extension` for unsupported dtype.
340    ///
341    /// # Deferred errors
342    ///
343    /// Symbolic axis and shape checks may be reported during compile or
344    /// execution.
345    fn norm(&self, ord: Option<f64>, dim: Option<&[usize]>, keepdim: bool) -> Result<TracedTensor>;
346}
347
348impl TracedTensorLinalgExt for TracedTensor {
349    fn svd(&self) -> Result<(TracedTensor, TracedTensor, TracedTensor)> {
350        svd(self)
351    }
352
353    fn svd_with_options(
354        &self,
355        options: SvdOptions,
356    ) -> Result<(TracedTensor, TracedTensor, TracedTensor)> {
357        svd_with_options(self, options)
358    }
359
360    fn svd_full(&self) -> Result<(TracedTensor, TracedTensor, TracedTensor)> {
361        svd_full(self)
362    }
363
364    fn qr(&self) -> Result<(TracedTensor, TracedTensor)> {
365        qr(self)
366    }
367
368    fn qr_with_options(&self, options: QrOptions) -> Result<(TracedTensor, TracedTensor)> {
369        qr_with_options(self, options)
370    }
371
372    fn eigh(&self) -> Result<(TracedTensor, TracedTensor)> {
373        eigh(self)
374    }
375
376    fn eigh_with_options(&self, options: EighOptions) -> Result<(TracedTensor, TracedTensor)> {
377        eigh_with_options(self, options)
378    }
379
380    fn cholesky(&self) -> Result<TracedTensor> {
381        cholesky(self)
382    }
383
384    fn lu(&self) -> Result<(TracedTensor, TracedTensor, TracedTensor, TracedTensor)> {
385        lu(self)
386    }
387
388    fn full_piv_lu(
389        &self,
390    ) -> Result<(
391        TracedTensor,
392        TracedTensor,
393        TracedTensor,
394        TracedTensor,
395        TracedTensor,
396    )> {
397        full_piv_lu(self)
398    }
399
400    fn eig(&self) -> Result<(TracedTensor, TracedTensor)> {
401        eig(self)
402    }
403
404    fn solve(&self, b: &TracedTensor) -> Result<TracedTensor> {
405        solve(self, b)
406    }
407
408    fn lstsq(&self, b: &TracedTensor) -> Result<TracedTensor> {
409        lstsq(self, b)
410    }
411
412    fn full_piv_lu_solve(&self, b: &TracedTensor) -> Result<TracedTensor> {
413        full_piv_lu_solve(self, b)
414    }
415
416    fn triangular_solve(
417        &self,
418        b: &TracedTensor,
419        left_side: bool,
420        lower: bool,
421        transpose_a: bool,
422        unit_diagonal: bool,
423    ) -> Result<TracedTensor> {
424        triangular_solve(self, b, left_side, lower, transpose_a, unit_diagonal)
425    }
426
427    fn slogdet(&self) -> Result<(TracedTensor, TracedTensor)> {
428        slogdet(self)
429    }
430
431    fn det(&self) -> Result<TracedTensor> {
432        det(self)
433    }
434
435    fn inv(&self) -> Result<TracedTensor> {
436        inv(self)
437    }
438
439    fn eigvalsh(&self) -> Result<TracedTensor> {
440        eigvalsh(self)
441    }
442
443    fn eigvals(&self) -> Result<TracedTensor> {
444        eigvals(self)
445    }
446
447    fn pinv(&self) -> Result<TracedTensor> {
448        pinv(self)
449    }
450
451    fn pinv_with_rtol(&self, rtol: f64) -> Result<TracedTensor> {
452        pinv_with_rtol(self, rtol)
453    }
454
455    fn norm(&self, ord: Option<f64>, dim: Option<&[usize]>, keepdim: bool) -> Result<TracedTensor> {
456        norm(self, ord, dim, keepdim)
457    }
458}
459
460/// Build a traced singular value decomposition op using default options.
461///
462/// # Examples
463///
464/// ```
465/// use tenferro_linalg::TracedTensorLinalgExt;
466/// use tenferro_runtime::TracedTensor;
467///
468/// let a = TracedTensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 0.0, 0.0, 1.0]).unwrap();
469/// let (u, s, vt) = a.svd().unwrap();
470/// assert_eq!(u.rank, 2);
471/// assert_eq!(s.rank, 1);
472/// assert_eq!(vt.rank, 2);
473/// ```
474///
475/// # Errors
476///
477/// Returns `Error::Validation` for a known invalid rank, matrix shape, or
478/// dtype, `Error::Extension` with an unsupported-dtype or non-convergence
479/// source when the registered linalg backend cannot construct the operation,
480/// and `Error::RuntimeState` when extension registration is unavailable.
481///
482/// # Deferred errors
483///
484/// A symbolic matrix or batch-shape mismatch is reported later as
485/// `ShapeConstraintViolation` or `ShapeConstraintEvaluation` during compile or
486/// execution.
487pub fn svd(a: &TracedTensor) -> Result<(TracedTensor, TracedTensor, TracedTensor)> {
488    svd_with_options(a, SvdOptions::default())
489}
490
491/// Build a traced singular value decomposition op with explicit options.
492///
493/// `derivative_eps` regularizes decomposition derivative formulas. It is not a
494/// backend SVD solver tolerance.
495///
496/// # Examples
497///
498/// ```
499/// use tenferro_linalg::{SvdGauge, SvdOptions, TracedTensorLinalgExt};
500/// use tenferro_runtime::TracedTensor;
501///
502/// let a = TracedTensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 0.0, 0.0, 1.0]).unwrap();
503/// let options = SvdOptions::default()
504///     .gauge(SvdGauge::CanonicalPivot)
505///     .derivative_eps(1e-10);
506/// let (_u, s, _vt) = a.svd_with_options(options).unwrap();
507/// assert_eq!(s.rank, 1);
508/// ```
509///
510/// # Errors
511///
512/// Returns `Error::Validation` when `derivative_eps` is non-finite or
513/// non-positive, `Error::Extension` for an unsupported dtype or numerical
514/// non-convergence, and `Error::Internal` if the extension output contract is
515/// violated.
516///
517/// # Deferred errors
518///
519/// Symbolic rank or shape constraints are checked later and can produce
520/// `ShapeConstraintViolation` or `ShapeConstraintEvaluation`.
521pub fn svd_with_options(
522    a: &TracedTensor,
523    options: SvdOptions,
524) -> Result<(TracedTensor, TracedTensor, TracedTensor)> {
525    validate_derivative_eps("svd_with_options", options.derivative_eps)?;
526    ensure_float_or_complex("svd", a.dtype)?;
527    three_outputs(
528        apply(
529            Arc::new(LinalgExtensionOp::new(LinalgOp::Svd {
530                derivative_eps: options.derivative_eps,
531                gauge: options.gauge,
532            })),
533            &[a],
534        )?,
535        "svd",
536    )
537}
538
539/// Build a traced full-matrices singular value decomposition op.
540///
541/// Unlike [`svd`], the returned factors are square: `U` is `m x m` and `Vh` is
542/// `n x n`, while `S` still holds `min(m, n)` singular values. The trailing
543/// `n - rank` rows of `Vh` span the right nullspace of the input, so this is
544/// the decomposition to use for kernel-basis extraction.
545///
546/// # Examples
547///
548/// ```
549/// use tenferro_linalg::TracedTensorLinalgExt;
550/// use tenferro_runtime::TracedTensor;
551///
552/// // A wide 1x2 system: the trailing row of the 2x2 Vh spans the nullspace.
553/// let a = TracedTensor::from_vec_col_major(vec![1, 2], vec![1.0_f64, 1.0]).unwrap();
554/// let (u, s, vh) = a.svd_full().unwrap();
555/// assert_eq!(u.rank, 2);
556/// assert_eq!(s.rank, 1);
557/// assert_eq!(vh.rank, 2);
558/// ```
559///
560/// # Errors
561///
562/// Returns `Error::Validation` when the input is not a batched matrix
563/// (rank `>= 2`) or `Error::Extension` with `ErrorKind::Unsupported` for
564/// integer or boolean dtypes; `Error::RuntimeState` when extension
565/// registration is unavailable.
566///
567/// # Deferred errors
568///
569/// The active backend returns `Error::Extension` with `ErrorKind::Unsupported`
570/// during execution if it does not implement full-matrices SVD (only the CPU
571/// faer provider does in this slice). Automatic differentiation is
572/// intentionally unsupported for the full variant (see the linalg AD support
573/// manifest) and surfaces a typed AD error, not a silent thin-SVD fallback.
574pub fn svd_full(a: &TracedTensor) -> Result<(TracedTensor, TracedTensor, TracedTensor)> {
575    ensure_float_or_complex("svd_full", a.dtype)?;
576    three_outputs(
577        apply(Arc::new(LinalgExtensionOp::new(LinalgOp::SvdFull)), &[a])?,
578        "svd_full",
579    )
580}
581
582/// Build a traced QR decomposition op.
583///
584/// # Examples
585///
586/// ```
587/// use tenferro_linalg::TracedTensorLinalgExt;
588/// use tenferro_runtime::TracedTensor;
589///
590/// let a = TracedTensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 0.0, 0.0, 1.0]).unwrap();
591/// let (q, r) = a.qr().unwrap();
592/// assert_eq!(q.rank, 2);
593/// assert_eq!(r.rank, 2);
594/// ```
595///
596/// # Errors
597///
598/// Returns `Error::Validation` for a known invalid rank or matrix shape,
599/// `Error::Extension` for an unsupported dtype or numerical failure, and
600/// `Error::RuntimeState` when the linalg extension is not registered.
601///
602/// # Deferred errors
603///
604/// Unknown matrix or batch dimensions can fail later as
605/// `ShapeConstraintViolation` or `ShapeConstraintEvaluation`.
606pub fn qr(a: &TracedTensor) -> Result<(TracedTensor, TracedTensor)> {
607    qr_with_options(a, QrOptions::default())
608}
609
610/// Build a traced QR decomposition op with explicit options.
611///
612/// `gauge` controls optional sign or phase post-processing.
613///
614/// # Examples
615///
616/// ```
617/// use tenferro_linalg::{QrGauge, QrOptions, TracedTensorLinalgExt};
618/// use tenferro_runtime::TracedTensor;
619///
620/// let a = TracedTensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 0.0, 0.0, 1.0]).unwrap();
621/// let (q, r) = a.qr_with_options(QrOptions::default().gauge(QrGauge::PositiveDiagonal)).unwrap();
622/// assert_eq!(q.rank, 2);
623/// assert_eq!(r.rank, 2);
624/// ```
625///
626/// # Errors
627///
628/// Returns `Error::Validation` for a known invalid rank or matrix shape,
629/// `Error::Extension` for an unsupported dtype or numerical failure, and
630/// `Error::Internal` if the extension output contract is violated.
631///
632/// # Deferred errors
633///
634/// Symbolic matrix or batch constraints are checked later and can produce
635/// `ShapeConstraintViolation` or `ShapeConstraintEvaluation`.
636pub fn qr_with_options(
637    a: &TracedTensor,
638    options: QrOptions,
639) -> Result<(TracedTensor, TracedTensor)> {
640    ensure_float_or_complex("qr", a.dtype)?;
641    two_outputs(
642        apply(
643            Arc::new(LinalgExtensionOp::new(LinalgOp::Qr {
644                gauge: options.gauge,
645            })),
646            &[a],
647        )?,
648        "qr",
649    )
650}
651
652/// Build a traced Hermitian eigenvalue decomposition op using default options.
653///
654/// # Examples
655///
656/// ```
657/// use tenferro_linalg::TracedTensorLinalgExt;
658/// use tenferro_runtime::TracedTensor;
659///
660/// let a = TracedTensor::from_vec_col_major(vec![2, 2], vec![2.0_f64, 0.0, 0.0, 3.0]).unwrap();
661/// let (values, vectors) = a.eigh().unwrap();
662/// assert_eq!(values.rank, 1);
663/// assert_eq!(vectors.rank, 2);
664/// ```
665///
666/// # Errors
667///
668/// Returns `Error::Validation` for a known non-square or invalid-rank input,
669/// `Error::Extension` for an unsupported dtype or eigensolver
670/// non-convergence, and `Error::RuntimeState` when the extension is not
671/// registered.
672///
673/// # Deferred errors
674///
675/// Symbolic square-shape constraints can fail later as
676/// `ShapeConstraintViolation` or `ShapeConstraintEvaluation`.
677pub fn eigh(a: &TracedTensor) -> Result<(TracedTensor, TracedTensor)> {
678    eigh_with_options(a, EighOptions::default())
679}
680
681/// Build a traced Hermitian eigenvalue decomposition op with explicit options.
682///
683/// `derivative_eps` regularizes derivative formulas for repeated or nearly
684/// repeated eigenvalues. It is not a backend eigensolver tolerance.
685///
686/// # Examples
687///
688/// ```
689/// use tenferro_linalg::{EighGauge, EighOptions, TracedTensorLinalgExt};
690/// use tenferro_runtime::TracedTensor;
691///
692/// let a = TracedTensor::from_vec_col_major(vec![2, 2], vec![2.0_f64, 0.0, 0.0, 3.0]).unwrap();
693/// let (values, _vectors) = a
694///     .eigh_with_options(
695///         EighOptions::default()
696///             .gauge(EighGauge::CanonicalPivot)
697///             .derivative_eps(1e-10),
698///     )
699///     .unwrap();
700/// assert_eq!(values.rank, 1);
701/// ```
702///
703/// # Errors
704///
705/// Returns `Error::Validation` for a known non-square or invalid-rank input,
706/// or for non-finite/non-positive `derivative_eps`; `Error::Extension` for an
707/// unsupported dtype or eigensolver non-convergence; and `Error::Internal` for
708/// an output-count contract violation.
709///
710/// # Deferred errors
711///
712/// Symbolic square-shape constraints can fail later as
713/// `ShapeConstraintViolation` or `ShapeConstraintEvaluation`.
714pub fn eigh_with_options(
715    a: &TracedTensor,
716    options: EighOptions,
717) -> Result<(TracedTensor, TracedTensor)> {
718    validate_derivative_eps("eigh_with_options", options.derivative_eps)?;
719    ensure_float_or_complex("eigh", a.dtype)?;
720    two_outputs(
721        apply(
722            Arc::new(LinalgExtensionOp::new(LinalgOp::Eigh {
723                derivative_eps: options.derivative_eps,
724                gauge: options.gauge,
725            })),
726            &[a],
727        )?,
728        "eigh",
729    )
730}
731
732/// Build a traced Cholesky decomposition op.
733///
734/// # Examples
735///
736/// ```
737/// use tenferro_linalg::TracedTensorLinalgExt;
738/// use tenferro_runtime::TracedTensor;
739///
740/// let a = TracedTensor::from_vec_col_major(vec![2, 2], vec![4.0_f64, 2.0, 2.0, 3.0]).unwrap();
741/// let factor = a.cholesky().unwrap();
742/// assert_eq!(factor.rank, 2);
743/// ```
744///
745/// # Errors
746///
747/// Returns `Error::Validation` for a known non-square or invalid-rank input,
748/// `Error::Extension` for an unsupported dtype or a non-positive-definite
749/// matrix, and `Error::RuntimeState` when the extension is not registered.
750///
751/// # Deferred errors
752///
753/// Symbolic square-shape constraints can fail later as
754/// `ShapeConstraintViolation` or `ShapeConstraintEvaluation`.
755pub fn cholesky(a: &TracedTensor) -> Result<TracedTensor> {
756    ensure_float_or_complex("cholesky", a.dtype)?;
757    one_output(
758        apply(Arc::new(LinalgExtensionOp::new(LinalgOp::Cholesky)), &[a])?,
759        "cholesky",
760    )
761}
762
763/// Build a traced LU decomposition op.
764///
765/// # Examples
766///
767/// ```
768/// use tenferro_linalg::TracedTensorLinalgExt;
769/// use tenferro_runtime::TracedTensor;
770///
771/// let a = TracedTensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 3.0, 2.0, 4.0]).unwrap();
772/// let (p, l, u, parity) = a.lu().unwrap();
773/// assert_eq!(p.rank, 2);
774/// assert_eq!(l.rank, 2);
775/// assert_eq!(u.rank, 2);
776/// assert_eq!(parity.rank, 0);
777/// ```
778///
779/// # Errors
780///
781/// Returns `Error::Validation` for a known invalid rank or matrix shape,
782/// `Error::Extension` for an unsupported dtype or singular numerical result,
783/// and `Error::RuntimeState` when the extension is not registered.
784///
785/// # Deferred errors
786///
787/// Symbolic square-shape constraints can fail later as
788/// `ShapeConstraintViolation` or `ShapeConstraintEvaluation`.
789pub fn lu(a: &TracedTensor) -> Result<(TracedTensor, TracedTensor, TracedTensor, TracedTensor)> {
790    four_outputs(
791        apply(Arc::new(LinalgExtensionOp::new(LinalgOp::Lu)), &[a])?,
792        "lu",
793    )
794}
795
796/// Build a traced full-pivot LU decomposition op.
797///
798/// Returns `(P, L, U, Q, parity)` with reconstruction convention
799/// `A = P^T * L * U * Q`, equivalently `P * A * Q^T = L * U`. `parity` is a
800/// scalar real tensor containing `+1` or `-1`: `F32` for `F32`/`C32` inputs and
801/// `F64` for `F64`/`C64` inputs.
802///
803/// # Examples
804///
805/// ```
806/// use tenferro_linalg::TracedTensorLinalgExt;
807/// use tenferro_runtime::TracedTensor;
808///
809/// let a = TracedTensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 3.0, 2.0, 4.0]).unwrap();
810/// let (p, l, u, q, parity) = a.full_piv_lu().unwrap();
811/// assert_eq!(p.rank, 2);
812/// assert_eq!(l.rank, 2);
813/// assert_eq!(u.rank, 2);
814/// assert_eq!(q.rank, 2);
815/// assert_eq!(parity.rank, 0);
816/// ```
817///
818/// # Errors
819///
820/// Returns `Error::Validation` for a known invalid rank or matrix shape,
821/// `Error::Extension` for an unsupported dtype or singular numerical result,
822/// and `Error::Internal` for an output-count contract violation.
823///
824/// # Deferred errors
825///
826/// Symbolic square-shape constraints can fail later as
827/// `ShapeConstraintViolation` or `ShapeConstraintEvaluation`.
828pub fn full_piv_lu(
829    a: &TracedTensor,
830) -> Result<(
831    TracedTensor,
832    TracedTensor,
833    TracedTensor,
834    TracedTensor,
835    TracedTensor,
836)> {
837    five_outputs(
838        apply(Arc::new(LinalgExtensionOp::new(LinalgOp::FullPivLu)), &[a])?,
839        "full_piv_lu",
840    )
841}
842
843/// Build a traced general eigendecomposition op.
844///
845/// # Examples
846///
847/// ```
848/// use tenferro_linalg::TracedTensorLinalgExt;
849/// use tenferro_runtime::TracedTensor;
850///
851/// let a = TracedTensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 0.0, 0.0, 2.0]).unwrap();
852/// let (values, vectors) = a.eig().unwrap();
853/// assert_eq!(values.rank, 1);
854/// assert_eq!(vectors.rank, 2);
855/// ```
856///
857/// # Errors
858///
859/// Returns `Error::Validation` for a known non-square or invalid-rank input,
860/// `Error::Extension` for an unsupported dtype or eigensolver
861/// non-convergence, and `Error::RuntimeState` when the extension is not
862/// registered.
863///
864/// # Deferred errors
865///
866/// Symbolic square-shape constraints can fail later as
867/// `ShapeConstraintViolation` or `ShapeConstraintEvaluation`.
868pub fn eig(a: &TracedTensor) -> Result<(TracedTensor, TracedTensor)> {
869    two_outputs(
870        apply(
871            Arc::new(LinalgExtensionOp::new(LinalgOp::Eig {
872                input_dtype: a.dtype,
873            })),
874            &[a],
875        )?,
876        "eig",
877    )
878}
879
880/// Build a traced linear solve op.
881///
882/// # Examples
883///
884/// ```
885/// use tenferro_linalg::TracedTensorLinalgExt;
886/// use tenferro_runtime::TracedTensor;
887///
888/// let a = TracedTensor::from_vec_col_major(vec![2, 2], vec![2.0_f64, 0.0, 0.0, 3.0]).unwrap();
889/// let b = TracedTensor::from_vec_col_major(vec![2, 1], vec![4.0_f64, 9.0]).unwrap();
890/// let x = a.solve(&b).unwrap();
891/// assert_eq!(x.rank, 2);
892/// ```
893///
894/// # Errors
895///
896/// Returns `Error::Validation` for known incompatible matrix, batch, or dtype
897/// metadata, `Error::Extension` for an unsupported dtype or singular system,
898/// and `Error::RuntimeState` when the extension is not registered.
899///
900/// # Deferred errors
901///
902/// Symbolic matrix and batch constraints can fail later as
903/// `ShapeConstraintViolation`, `ShapeConstraintEvaluation`, or
904/// `ShapeExpressionEvaluation`.
905pub fn solve(a: &TracedTensor, b: &TracedTensor) -> Result<TracedTensor> {
906    let mut factor_outputs =
907        apply(Arc::new(LinalgExtensionOp::new(LinalgOp::LuFactor)), &[a])?.into_iter();
908    let (packed_lu, pivots) = match (
909        factor_outputs.next(),
910        factor_outputs.next(),
911        factor_outputs.next(),
912        factor_outputs.next(),
913    ) {
914        (Some(packed_lu), Some(pivots), Some(_parity), None) => (packed_lu, pivots),
915        _ => return Err(unexpected_output_count("lu_factor", 3)),
916    };
917    one_output(
918        apply(
919            Arc::new(LinalgExtensionOp::new(LinalgOp::LuSolvePrepared {
920                transpose_a: false,
921                conjugate_a: false,
922            })),
923            &[a, &packed_lu, &pivots, b],
924        )?,
925        "solve",
926    )
927}
928
929/// Build a traced least-squares solve `argmin_x ||A x - b||_2` for a tall or
930/// square, full-column-rank `A`.
931///
932/// The solution is computed through the thin QR factorization `A = Q R`: since
933/// `R` is nonsingular for full column rank, `x = R^{-1} (Qá´´ b)`. This composes
934/// existing traced decomposition ops (`qr`, `dot_general`, `triangular_solve`),
935/// so, unlike the value-only [`svd_full`], it participates in autodiff through
936/// its component rules.
937///
938/// # Examples
939///
940/// ```
941/// use tenferro_linalg::TracedTensorLinalgExt;
942/// use tenferro_runtime::TracedTensor;
943///
944/// // Overdetermined 3x2 system.
945/// let a = TracedTensor::from_vec_col_major(
946///     vec![3, 2],
947///     vec![1.0_f64, 1.0, 1.0, 0.0, 1.0, 2.0],
948/// )
949/// .unwrap();
950/// let b = TracedTensor::from_vec_col_major(vec![3, 1], vec![1.0_f64, 2.0, 2.0]).unwrap();
951/// let x = a.lstsq(&b).unwrap();
952/// assert_eq!(x.rank, 2);
953/// ```
954///
955/// # Errors
956///
957/// Returns `Error::Validation` when `A` or `b` is not a batched matrix
958/// (rank `>= 2`), when `A` has a symbolic shape, when `A` is wide
959/// (`rows < cols`, underdetermined), or when the dtype is not floating-point or
960/// complex. Rank-deficient `A` is not detected here: `R` is singular and the
961/// triangular solve yields a non-finite or ill-defined result, so callers must
962/// ensure full column rank.
963///
964/// # Deferred errors
965///
966/// Backend QR and triangular-solve failures and concrete shape mismatches are
967/// reported during compile or execution.
968pub fn lstsq(a: &TracedTensor, b: &TracedTensor) -> Result<TracedTensor> {
969    validate_lstsq(
970        "lstsq",
971        a.dtype,
972        a.rank,
973        b.rank,
974        || {
975            let shape = require_concrete_shape("lstsq", a)?;
976            Ok((shape[0], shape[1]))
977        },
978        |message| {
979            Error::TensorRuntime(tenferro_tensor::Error::invalid_argument(
980                "lstsq", "shape", message,
981            ))
982        },
983    )?;
984    let (q, r) = qr(a)?;
985    let qh = q.conj()?.transpose(&matrix_transpose_perm(q.rank))?;
986    let qh_b = matmul_preserve_trailing_batch(&qh, b)?;
987    triangular_solve(&r, &qh_b, true, false, false, false)
988}
989
990/// Build a traced full-pivot LU solve op.
991///
992/// # Examples
993///
994/// ```
995/// use tenferro_linalg::TracedTensorLinalgExt;
996/// use tenferro_runtime::TracedTensor;
997///
998/// let a = TracedTensor::from_vec_col_major(vec![2, 2], vec![2.0_f64, 0.0, 0.0, 3.0]).unwrap();
999/// let b = TracedTensor::from_vec_col_major(vec![2, 1], vec![4.0_f64, 9.0]).unwrap();
1000/// let x = a.full_piv_lu_solve(&b).unwrap();
1001/// assert_eq!(x.rank, 2);
1002/// ```
1003///
1004/// # Errors
1005///
1006/// Returns `Error::Validation` for known incompatible matrix, batch, or dtype
1007/// metadata, `Error::Extension` for an unsupported dtype or singular system,
1008/// and `Error::RuntimeState` when the extension is not registered.
1009///
1010/// # Deferred errors
1011///
1012/// Symbolic matrix and batch constraints can fail later as
1013/// `ShapeConstraintViolation`, `ShapeConstraintEvaluation`, or
1014/// `ShapeExpressionEvaluation`.
1015pub fn full_piv_lu_solve(a: &TracedTensor, b: &TracedTensor) -> Result<TracedTensor> {
1016    one_output(
1017        apply(
1018            Arc::new(LinalgExtensionOp::new(LinalgOp::FullPivLuSolve {
1019                transpose_a: false,
1020            })),
1021            &[a, b],
1022        )?,
1023        "full_piv_lu_solve",
1024    )
1025}
1026
1027/// Build a traced triangular solve op.
1028///
1029/// # Examples
1030///
1031/// ```
1032/// use tenferro_linalg::TracedTensorLinalgExt;
1033/// use tenferro_runtime::TracedTensor;
1034///
1035/// let a = TracedTensor::from_vec_col_major(vec![2, 2], vec![2.0_f64, 0.0, 1.0, 3.0]).unwrap();
1036/// let b = TracedTensor::from_vec_col_major(vec![2, 1], vec![4.0_f64, 9.0]).unwrap();
1037/// let x = a.triangular_solve(&b, true, true, false, false).unwrap();
1038/// assert_eq!(x.rank, 2);
1039/// ```
1040///
1041/// # Errors
1042///
1043/// Returns `Error::Validation` for incompatible matrix, batch, or dtype
1044/// metadata, `Error::Extension` for an unsupported dtype or singular system,
1045/// and `Error::RuntimeState` when the extension is not registered.
1046///
1047/// # Deferred errors
1048///
1049/// Symbolic matrix and batch constraints can fail later as
1050/// `ShapeConstraintViolation`, `ShapeConstraintEvaluation`, or
1051/// `ShapeExpressionEvaluation`.
1052pub fn triangular_solve(
1053    a: &TracedTensor,
1054    b: &TracedTensor,
1055    left_side: bool,
1056    lower: bool,
1057    transpose_a: bool,
1058    unit_diagonal: bool,
1059) -> Result<TracedTensor> {
1060    one_output(
1061        apply(
1062            Arc::new(LinalgExtensionOp::new(LinalgOp::TriangularSolve {
1063                left_side,
1064                lower,
1065                transpose_a,
1066                unit_diagonal,
1067            })),
1068            &[a, b],
1069        )?,
1070        "triangular_solve",
1071    )
1072}
1073
1074/// Build traced sign and log-absolute-determinant ops.
1075///
1076/// # Examples
1077///
1078/// ```
1079/// use tenferro_linalg::TracedTensorLinalgExt;
1080/// use tenferro_runtime::TracedTensor;
1081///
1082/// let a = TracedTensor::from_vec_col_major(vec![2, 2], vec![2.0_f64, 0.0, 0.0, 3.0]).unwrap();
1083/// let (sign, logabsdet) = a.slogdet().unwrap();
1084/// assert_eq!(sign.rank, 0);
1085/// assert_eq!(logabsdet.rank, 0);
1086/// ```
1087///
1088/// # Errors
1089///
1090/// Returns `Error::Validation` for a known non-square or invalid-rank input,
1091/// `Error::Extension` for an unsupported dtype or singular factorization, and
1092/// `Error::Internal` if the factorization output contract is violated.
1093///
1094/// # Deferred errors
1095///
1096/// Symbolic square-shape constraints can fail later as
1097/// `ShapeConstraintViolation` or `ShapeConstraintEvaluation`.
1098pub fn slogdet(a: &TracedTensor) -> Result<(TracedTensor, TracedTensor)> {
1099    if let Some(empty) = slogdet_empty_square(a)? {
1100        return Ok(empty);
1101    }
1102    let mut factor_outputs =
1103        apply(Arc::new(LinalgExtensionOp::new(LinalgOp::LuFactor)), &[a])?.into_iter();
1104    let (packed_lu, parity) = match (
1105        factor_outputs.next(),
1106        factor_outputs.next(),
1107        factor_outputs.next(),
1108        factor_outputs.next(),
1109    ) {
1110        (Some(packed_lu), Some(_pivots), Some(parity), None) => (packed_lu, parity),
1111        _ => return Err(unexpected_output_count("lu_factor", 3)),
1112    };
1113    let mut sign_outputs = apply(
1114        Arc::new(LinalgExtensionOp::new(LinalgOp::SignDetFromLuFactor)),
1115        &[a, &packed_lu, &parity],
1116    )?
1117    .into_iter();
1118    let sign = match (sign_outputs.next(), sign_outputs.next()) {
1119        (Some(sign), None) => sign,
1120        _ => return Err(unexpected_output_count("signdet_from_lu_factor", 1)),
1121    };
1122    let mut logabsdet_outputs = apply(
1123        Arc::new(LinalgExtensionOp::new(LinalgOp::LogAbsDetFromLuFactor)),
1124        &[a, &packed_lu],
1125    )?
1126    .into_iter();
1127    let logabsdet = match (logabsdet_outputs.next(), logabsdet_outputs.next()) {
1128        (Some(logabsdet), None) => logabsdet,
1129        _ => return Err(unexpected_output_count("logabsdet_from_lu_factor", 1)),
1130    };
1131    Ok((sign, logabsdet))
1132}
1133
1134/// Build a traced determinant op.
1135///
1136/// # Examples
1137///
1138/// ```
1139/// use tenferro_linalg::TracedTensorLinalgExt;
1140/// use tenferro_runtime::TracedTensor;
1141///
1142/// let a = TracedTensor::from_vec_col_major(vec![2, 2], vec![2.0_f64, 0.0, 0.0, 3.0]).unwrap();
1143/// let determinant = a.det().unwrap();
1144/// assert_eq!(determinant.rank, 0);
1145/// ```
1146///
1147/// # Errors
1148///
1149/// Returns the same `Error::Validation`, `Error::Extension`, and
1150/// `Error::RuntimeState` failures as [`slogdet`], including a singular
1151/// factorization and an invalid matrix shape.
1152///
1153/// # Deferred errors
1154///
1155/// Symbolic shape checks can later produce `ShapeConstraintViolation`,
1156/// `ShapeConstraintEvaluation`, or `ShapeExpressionEvaluation`.
1157pub fn det(a: &TracedTensor) -> Result<TracedTensor> {
1158    if let Some((det, _logabsdet)) = slogdet_empty_square(a)? {
1159        return Ok(det);
1160    }
1161    let (_p, _l, u, parity) = lu(a)?;
1162    let diag_u = u.extract_diag(0, 1)?;
1163    let det_u = diag_u.reduce_prod(Some(&[0]))?;
1164    &parity * &det_u
1165}
1166
1167fn slogdet_empty_square(a: &TracedTensor) -> Result<Option<(TracedTensor, TracedTensor)>> {
1168    let Some(shape) = a.try_concrete_shape() else {
1169        return Ok(None);
1170    };
1171    if shape.len() < 2 || shape[0] != 0 || shape[1] != 0 {
1172        return Ok(None);
1173    }
1174    let batch_shape = shape[2..].to_vec();
1175    Ok(Some((
1176        filled_real(a.dtype, batch_shape.clone(), 1.0)?,
1177        filled_real(real_values_dtype(a.dtype), batch_shape, 0.0)?,
1178    )))
1179}
1180
1181/// Build a traced matrix inverse op.
1182///
1183/// # Examples
1184///
1185/// ```
1186/// use tenferro_linalg::TracedTensorLinalgExt;
1187/// use tenferro_runtime::TracedTensor;
1188///
1189/// let a = TracedTensor::from_vec_col_major(vec![2, 2], vec![2.0_f64, 0.0, 0.0, 3.0]).unwrap();
1190/// let inverse = a.inv().unwrap();
1191/// assert_eq!(inverse.rank, 2);
1192/// ```
1193///
1194/// # Errors
1195///
1196/// Returns `Error::Validation` when the input is not at least rank two or is
1197/// not square, `Error::Extension` for an unsupported dtype or singular system,
1198/// and `Error::RuntimeState` when the extension is not registered.
1199///
1200/// # Deferred errors
1201///
1202/// A symbolic shape that cannot provide the identity size fails later as
1203/// `ShapeConstraintEvaluation` or `ShapeExpressionEvaluation`.
1204pub fn inv(a: &TracedTensor) -> Result<TracedTensor> {
1205    ensure_min_rank("inv", a.rank, 2)?;
1206    let shape = require_concrete_shape("inv", a)?;
1207    let eye = eye_like(a, shape[0])?;
1208    solve(a, &eye)
1209}
1210
1211/// Build a traced Hermitian eigenvalue-only op.
1212///
1213/// # Examples
1214///
1215/// ```
1216/// use tenferro_linalg::TracedTensorLinalgExt;
1217/// use tenferro_runtime::TracedTensor;
1218///
1219/// let a = TracedTensor::from_vec_col_major(vec![2, 2], vec![2.0_f64, 0.0, 0.0, 3.0]).unwrap();
1220/// let values = a.eigvalsh().unwrap();
1221/// assert_eq!(values.rank, 1);
1222/// ```
1223///
1224/// # Errors
1225///
1226/// Returns `Error::Validation` for a known non-square or invalid-rank input,
1227/// `Error::Extension` for an unsupported dtype or eigensolver
1228/// non-convergence, and `Error::RuntimeState` when the extension is not
1229/// registered.
1230///
1231/// # Deferred errors
1232///
1233/// Symbolic square-shape constraints can fail later as
1234/// `ShapeConstraintViolation` or `ShapeConstraintEvaluation`.
1235pub fn eigvalsh(a: &TracedTensor) -> Result<TracedTensor> {
1236    ensure_float_or_complex("eigvalsh", a.dtype)?;
1237    eigh_values(a)
1238}
1239
1240/// Build a traced general eigenvalue-only op.
1241///
1242/// # Examples
1243///
1244/// ```
1245/// use tenferro_linalg::TracedTensorLinalgExt;
1246/// use tenferro_runtime::TracedTensor;
1247///
1248/// let a = TracedTensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 0.0, 0.0, 2.0]).unwrap();
1249/// let values = a.eigvals().unwrap();
1250/// assert_eq!(values.rank, 1);
1251/// ```
1252///
1253/// # Errors
1254///
1255/// Returns `Error::Validation` for a known non-square or invalid-rank input,
1256/// `Error::Extension` for an unsupported dtype or eigensolver
1257/// non-convergence, and `Error::RuntimeState` when the extension is not
1258/// registered.
1259///
1260/// # Deferred errors
1261///
1262/// Symbolic square-shape constraints can fail later as
1263/// `ShapeConstraintViolation` or `ShapeConstraintEvaluation`.
1264pub fn eigvals(a: &TracedTensor) -> Result<TracedTensor> {
1265    eig_values(a)
1266}
1267
1268/// Build a traced Moore-Penrose pseudoinverse op.
1269///
1270/// Floating-point and complex inputs are supported. Integer and boolean inputs
1271/// return an unsupported-dtype error.
1272///
1273/// # Examples
1274///
1275/// ```
1276/// use tenferro_linalg::TracedTensorLinalgExt;
1277/// use tenferro_runtime::TracedTensor;
1278///
1279/// let a = TracedTensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 0.0, 0.0, 2.0]).unwrap();
1280/// let inverse = a.pinv().unwrap();
1281/// assert_eq!(inverse.rank, 2);
1282/// ```
1283///
1284/// # Errors
1285///
1286/// Returns `Error::Validation` for an invalid rank, shape, or negative/non-
1287/// finite `rtol`, `Error::Extension` for unsupported integer or boolean dtypes,
1288/// numerical non-convergence, or a backend failure, and `Error::RuntimeState`
1289/// when the extension is not registered.
1290///
1291/// # Deferred errors
1292///
1293/// Symbolic shapes are materialized by this helper; failures are reported as
1294/// `ShapeConstraintEvaluation` or `ShapeExpressionEvaluation`.
1295pub fn pinv(a: &TracedTensor) -> Result<TracedTensor> {
1296    ensure_float_or_complex("pinv", a.dtype)?;
1297    let shape = require_concrete_shape("pinv", a)?;
1298    let max_dim = match (shape.first(), shape.get(1)) {
1299        (Some(&m), Some(&n)) => m.max(n),
1300        (Some(&m), None) => m,
1301        _ => 0,
1302    };
1303    pinv_with_rtol(a, default_pinv_rtol(a.dtype, max_dim))
1304}
1305
1306/// Build a traced Moore-Penrose pseudoinverse op with an explicit relative tolerance.
1307///
1308/// Floating-point and complex inputs are supported. Integer and boolean inputs
1309/// return an unsupported-dtype error.
1310///
1311/// # Examples
1312///
1313/// ```
1314/// use tenferro_linalg::TracedTensorLinalgExt;
1315/// use tenferro_runtime::TracedTensor;
1316///
1317/// let a = TracedTensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 0.0, 0.0, 2.0]).unwrap();
1318/// let inverse = a.pinv_with_rtol(1e-12).unwrap();
1319/// assert_eq!(inverse.rank, 2);
1320/// ```
1321///
1322/// # Errors
1323///
1324/// Returns `Error::Validation` for an invalid rank, shape, or non-finite
1325/// `rtol`, `Error::Extension` for unsupported integer or boolean dtypes,
1326/// numerical non-convergence, or a backend failure, and `Error::RuntimeState`
1327/// when the extension is not registered.
1328///
1329/// # Deferred errors
1330///
1331/// Symbolic shapes are materialized by this helper; failures are reported as
1332/// `ShapeConstraintEvaluation` or `ShapeExpressionEvaluation`.
1333pub fn pinv_with_rtol(a: &TracedTensor, rtol: f64) -> Result<TracedTensor> {
1334    ensure_float_or_complex("pinv_with_rtol", a.dtype)?;
1335    require_concrete_shape("pinv_with_rtol", a)?;
1336    let (u, s, vt) = svd(a)?;
1337    let abs_s = s.abs()?;
1338    let s_max = abs_s.reduce_max(Some(&[0]))?;
1339    let s_max_shape = s_max.concrete_shape()?;
1340    let threshold_scalar = broadcast_scalar(scalar_real(s.dtype, rtol.max(0.0))?, &s_max_shape)?;
1341    let threshold = (&s_max * &threshold_scalar)?;
1342    let s_shape = s.concrete_shape()?;
1343    let threshold = broadcast_batch_scalar_to_leading_axis(&threshold, &s_shape)?;
1344    let mask = abs_s.compare(&threshold, CompareDir::Gt)?;
1345    let mask = mask.convert(s.dtype)?;
1346    let ones = ones_like(&s)?;
1347    let neg_mask = (-&mask)?;
1348    let denom = (&s + &(&ones + &neg_mask)?)?;
1349    let s_inv = (&mask / &denom)?;
1350
1351    let v = vt.conj()?.transpose(&matrix_transpose_perm(vt.rank))?;
1352    let uh = u.conj()?.transpose(&matrix_transpose_perm(u.rank))?;
1353    let vs = scale_matrix_columns(&v, &s_inv)?;
1354    matmul_preserve_trailing_batch(&vs, &uh)
1355}
1356
1357/// Build a traced vector, matrix, or tensor norm op.
1358///
1359/// Floating-point and complex inputs are supported. Integer and boolean inputs
1360/// return an unsupported-dtype error.
1361///
1362/// # Examples
1363///
1364/// ```
1365/// use tenferro_linalg::TracedTensorLinalgExt;
1366/// use tenferro_runtime::TracedTensor;
1367///
1368/// let x = TracedTensor::from_vec_col_major(vec![3], vec![1.0_f64, 2.0, 3.0]).unwrap();
1369/// let length = x.norm(Some(2.0), Some(&[0]), false).unwrap();
1370/// assert_eq!(length.rank, 0);
1371/// ```
1372///
1373/// # Errors
1374///
1375/// Returns `Error::Validation` for an invalid axis, rank, or norm order,
1376/// `Error::Extension` for unsupported integer or boolean dtypes or a backend
1377/// numerical failure, and `Error::RuntimeState` when the extension is not
1378/// registered.
1379///
1380/// # Deferred errors
1381///
1382/// Symbolic shapes needed to restore `keepdim` are evaluated later and can
1383/// produce `ShapeConstraintEvaluation` or `ShapeExpressionEvaluation`.
1384pub fn norm(
1385    a: &TracedTensor,
1386    ord: Option<f64>,
1387    dim: Option<&[usize]>,
1388    keepdim: bool,
1389) -> Result<TracedTensor> {
1390    ensure_float_or_complex("norm", a.dtype)?;
1391    let shape = require_concrete_shape("norm", a)?;
1392    let axes = dim.map_or_else(|| (0..a.rank).collect::<Vec<_>>(), |dims| dims.to_vec());
1393    if axes.is_empty() {
1394        return Ok(a.clone());
1395    }
1396    validate_axes("norm", a.rank, &axes)?;
1397    if reduced_axes_have_zero_extent(&shape, &axes) {
1398        if let Some(zero) = zero_norm_for_empty_reduction(a.dtype, &shape, &axes, keepdim, ord)? {
1399            return Ok(zero);
1400        }
1401    }
1402
1403    let out = if can_square_without_abs(a.dtype, axes.len(), ord) {
1404        frobenius_norm(a, &axes)?
1405    } else {
1406        match axes.len() {
1407            1 => vector_norm(a, axes[0], ord)?,
1408            2 => matrix_norm(a, &axes, ord)?,
1409            _ => {
1410                let abs = a.abs()?;
1411                match ord {
1412                    None => frobenius_norm(&abs, &axes)?,
1413                    Some(p) if p == f64::INFINITY => abs.reduce_max(Some(&axes))?,
1414                    Some(p) if p == f64::NEG_INFINITY => abs.reduce_min(Some(&axes))?,
1415                    Some(0.0) => count_nonzero(&abs, &axes)?,
1416                    Some(p) => p_norm(&abs, &axes, p)?,
1417                }
1418            }
1419        }
1420    };
1421    restore_keepdim(out, &shape, &axes, keepdim)
1422}
1423
1424fn unexpected_output_count(name: &str, expected: usize) -> Error {
1425    Error::Internal(format!("{name} must produce exactly {expected} outputs"))
1426}
1427
1428fn one_output(outputs: Vec<TracedTensor>, name: &str) -> Result<TracedTensor> {
1429    let mut outputs = outputs.into_iter();
1430    match (outputs.next(), outputs.next()) {
1431        (Some(output), None) => Ok(output),
1432        _ => Err(unexpected_output_count(name, 1)),
1433    }
1434}
1435
1436fn two_outputs(outputs: Vec<TracedTensor>, name: &str) -> Result<(TracedTensor, TracedTensor)> {
1437    let mut outputs = outputs.into_iter();
1438    match (outputs.next(), outputs.next(), outputs.next()) {
1439        (Some(lhs), Some(rhs), None) => Ok((lhs, rhs)),
1440        _ => Err(unexpected_output_count(name, 2)),
1441    }
1442}
1443
1444fn three_outputs(
1445    outputs: Vec<TracedTensor>,
1446    name: &str,
1447) -> Result<(TracedTensor, TracedTensor, TracedTensor)> {
1448    let mut outputs = outputs.into_iter();
1449    match (
1450        outputs.next(),
1451        outputs.next(),
1452        outputs.next(),
1453        outputs.next(),
1454    ) {
1455        (Some(first), Some(second), Some(third), None) => Ok((first, second, third)),
1456        _ => Err(unexpected_output_count(name, 3)),
1457    }
1458}
1459
1460fn four_outputs(
1461    outputs: Vec<TracedTensor>,
1462    name: &str,
1463) -> Result<(TracedTensor, TracedTensor, TracedTensor, TracedTensor)> {
1464    let mut outputs = outputs.into_iter();
1465    match (
1466        outputs.next(),
1467        outputs.next(),
1468        outputs.next(),
1469        outputs.next(),
1470        outputs.next(),
1471    ) {
1472        (Some(first), Some(second), Some(third), Some(fourth), None) => {
1473            Ok((first, second, third, fourth))
1474        }
1475        _ => Err(unexpected_output_count(name, 4)),
1476    }
1477}
1478
1479fn five_outputs(
1480    outputs: Vec<TracedTensor>,
1481    name: &str,
1482) -> Result<(
1483    TracedTensor,
1484    TracedTensor,
1485    TracedTensor,
1486    TracedTensor,
1487    TracedTensor,
1488)> {
1489    let mut outputs = outputs.into_iter();
1490    match (
1491        outputs.next(),
1492        outputs.next(),
1493        outputs.next(),
1494        outputs.next(),
1495        outputs.next(),
1496        outputs.next(),
1497    ) {
1498        (Some(first), Some(second), Some(third), Some(fourth), Some(fifth), None) => {
1499            Ok((first, second, third, fourth, fifth))
1500        }
1501        _ => Err(unexpected_output_count(name, 5)),
1502    }
1503}
1504
1505fn scalar_real(dtype: DType, value: f64) -> Result<TracedTensor> {
1506    match dtype {
1507        DType::F64 => TracedTensor::from_vec_col_major(vec![], vec![value]),
1508        DType::F32 => TracedTensor::from_vec_col_major(vec![], vec![value as f32]),
1509        DType::I32 => TracedTensor::from_vec_col_major(vec![], vec![value.round() as i32]),
1510        DType::I64 => TracedTensor::from_vec_col_major(vec![], vec![value.round() as i64]),
1511        DType::Bool => TracedTensor::from_vec_col_major(vec![], vec![value != 0.0]),
1512        DType::C64 => TracedTensor::from_vec_col_major(vec![], vec![Complex64::new(value, 0.0)]),
1513        DType::C32 => {
1514            TracedTensor::from_vec_col_major(vec![], vec![Complex32::new(value as f32, 0.0)])
1515        }
1516    }
1517}
1518
1519fn filled_real(dtype: DType, shape: Vec<usize>, value: f64) -> Result<TracedTensor> {
1520    let len = tenferro_tensor::validate::checked_shape_product("slogdet", "output shape", &shape)?;
1521    match dtype {
1522        DType::F64 => TracedTensor::from_vec_col_major(shape, vec![value; len]),
1523        DType::F32 => TracedTensor::from_vec_col_major(shape, vec![value as f32; len]),
1524        DType::I32 => TracedTensor::from_vec_col_major(shape, vec![value.round() as i32; len]),
1525        DType::I64 => TracedTensor::from_vec_col_major(shape, vec![value.round() as i64; len]),
1526        DType::Bool => TracedTensor::from_vec_col_major(shape, vec![value != 0.0; len]),
1527        DType::C64 => {
1528            TracedTensor::from_vec_col_major(shape, vec![Complex64::new(value, 0.0); len])
1529        }
1530        DType::C32 => {
1531            TracedTensor::from_vec_col_major(shape, vec![Complex32::new(value as f32, 0.0); len])
1532        }
1533    }
1534}
1535
1536fn real_values_dtype(dtype: DType) -> DType {
1537    match dtype {
1538        DType::C64 => DType::F64,
1539        DType::C32 => DType::F32,
1540        other => other,
1541    }
1542}
1543
1544fn can_square_without_abs(dtype: DType, axes_len: usize, ord: Option<f64>) -> bool {
1545    matches!(dtype, DType::F32 | DType::F64)
1546        && (ord.is_none() || (ord == Some(2.0) && axes_len != 2))
1547}
1548
1549fn ensure_min_rank(op: &'static str, actual: usize, expected: usize) -> Result<()> {
1550    if actual < expected {
1551        return Err(Error::TensorRuntime(tenferro_tensor::Error::rank_mismatch(
1552            op, expected, actual,
1553        )));
1554    }
1555    Ok(())
1556}
1557
1558fn validate_axes(op: &'static str, rank: usize, axes: &[usize]) -> Result<()> {
1559    for &axis in axes {
1560        if axis >= rank {
1561            return Err(Error::TensorRuntime(
1562                tenferro_tensor::Error::axis_out_of_bounds(op, axis, rank),
1563            ));
1564        }
1565    }
1566    Ok(())
1567}
1568
1569fn require_concrete_shape(op: &'static str, input: &TracedTensor) -> Result<Vec<usize>> {
1570    input.try_concrete_shape().ok_or_else(|| {
1571        Error::TensorRuntime(tenferro_tensor::Error::invalid_argument(
1572            op,
1573            "shape",
1574            "symbolic shape is not supported by this traced linalg helper",
1575        ))
1576    })
1577}
1578
1579fn zero_scalar(dtype: DType) -> Result<TracedTensor> {
1580    scalar_real(dtype, 0.0)
1581}
1582
1583fn one_scalar(dtype: DType) -> Result<TracedTensor> {
1584    scalar_real(dtype, 1.0)
1585}
1586
1587fn ones_like(input: &TracedTensor) -> Result<TracedTensor> {
1588    let shape = input.concrete_shape()?;
1589    broadcast_scalar(one_scalar(input.dtype)?, &shape)
1590}
1591
1592fn eye_like(anchor: &TracedTensor, size: usize) -> Result<TracedTensor> {
1593    let mut vector_shape = vec![size];
1594    let anchor_shape = anchor.concrete_shape()?;
1595    vector_shape.extend_from_slice(&anchor_shape[2..]);
1596    let diagonal = broadcast_scalar(one_scalar(anchor.dtype)?, &vector_shape)?;
1597    diagonal.embed_diag(0, 1)
1598}
1599
1600fn broadcast_scalar(input: TracedTensor, shape: &[usize]) -> Result<TracedTensor> {
1601    let input_shape = input.concrete_shape()?;
1602    if input_shape == shape {
1603        return Ok(input);
1604    }
1605    input.broadcast_in_dim(shape, &[])
1606}
1607
1608fn broadcast_batch_scalar_to_leading_axis(
1609    input: &TracedTensor,
1610    shape: &[usize],
1611) -> Result<TracedTensor> {
1612    let input_shape = input.concrete_shape()?;
1613    if input_shape == shape {
1614        return Ok(input.clone());
1615    }
1616    let dims: Vec<usize> = (1..shape.len()).collect();
1617    input.broadcast_in_dim(shape, &dims)
1618}
1619
1620fn matmul_preserve_trailing_batch(lhs: &TracedTensor, rhs: &TracedTensor) -> Result<TracedTensor> {
1621    let rank = lhs.rank;
1622    let batch_dims: Vec<usize> = (2..rank).collect();
1623    lhs.dot_general(
1624        rhs,
1625        DotGeneralConfig {
1626            lhs_contracting_dims: vec![1],
1627            rhs_contracting_dims: vec![0],
1628            lhs_batch_dims: batch_dims.clone(),
1629            rhs_batch_dims: batch_dims,
1630        },
1631    )
1632}
1633
1634fn matrix_transpose_perm(rank: usize) -> Vec<usize> {
1635    let mut perm: Vec<usize> = (0..rank).collect();
1636    perm.swap(0, 1);
1637    perm
1638}
1639
1640fn frobenius_norm(abs: &TracedTensor, axes: &[usize]) -> Result<TracedTensor> {
1641    abs.reduce_sum_squares(axes)?.sqrt()
1642}
1643
1644fn p_norm(abs: &TracedTensor, axes: &[usize], p: f64) -> Result<TracedTensor> {
1645    if !p.is_finite() || p == 0.0 {
1646        return Err(Error::invalid_argument(
1647            "norm",
1648            ErrorPhase::GraphBuild,
1649            "p",
1650            format!("p-norm order must be finite and nonzero, got {p}"),
1651        ));
1652    }
1653    if p == 2.0 {
1654        return frobenius_norm(abs, axes);
1655    }
1656    let power = abs.pow(&scalar_real(abs.dtype, p)?)?;
1657    let inv_p = scalar_real(abs.dtype, 1.0 / p)?;
1658    power.reduce_sum(Some(axes))?.pow(&inv_p)
1659}
1660
1661fn reduced_axes_have_zero_extent(shape: &[usize], axes: &[usize]) -> bool {
1662    axes.iter().any(|&axis| shape[axis] == 0)
1663}
1664
1665fn zero_norm_for_empty_reduction(
1666    dtype: DType,
1667    input_shape: &[usize],
1668    axes: &[usize],
1669    keepdim: bool,
1670    ord: Option<f64>,
1671) -> Result<Option<TracedTensor>> {
1672    if !empty_reduction_norm_is_zero(axes.len(), ord) {
1673        return Ok(None);
1674    }
1675    let output_shape = reduction_shape(input_shape, axes, keepdim);
1676    zero_traced_tensor(real_norm_dtype(dtype)?, output_shape).map(Some)
1677}
1678
1679fn empty_reduction_norm_is_zero(axis_count: usize, ord: Option<f64>) -> bool {
1680    match ord {
1681        None => true,
1682        Some(0.0) => true,
1683        Some(p) if p.is_infinite() => true,
1684        Some(p) if p.is_finite() && p > 0.0 => axis_count != 2 || p != 2.0,
1685        _ => false,
1686    }
1687}
1688
1689fn reduction_shape(input_shape: &[usize], axes: &[usize], keepdim: bool) -> Vec<usize> {
1690    if keepdim {
1691        let mut shape = input_shape.to_vec();
1692        for &axis in axes {
1693            shape[axis] = 1;
1694        }
1695        return shape;
1696    }
1697    let mut reduced = vec![false; input_shape.len()];
1698    for &axis in axes {
1699        reduced[axis] = true;
1700    }
1701    input_shape
1702        .iter()
1703        .enumerate()
1704        .filter_map(|(axis, &dim)| (!reduced[axis]).then_some(dim))
1705        .collect()
1706}
1707
1708fn real_norm_dtype(dtype: DType) -> Result<DType> {
1709    match dtype {
1710        DType::F32 | DType::F64 => Ok(dtype),
1711        DType::C32 => Ok(DType::F32),
1712        DType::C64 => Ok(DType::F64),
1713        _ => Err(Error::TensorRuntime(
1714            tenferro_tensor::Error::unsupported_dtype(
1715                "norm",
1716                dtype,
1717                "norm supports only floating-point and complex dtypes",
1718            ),
1719        )),
1720    }
1721}
1722
1723fn zero_traced_tensor(dtype: DType, shape: Vec<usize>) -> Result<TracedTensor> {
1724    let len = checked_element_count("norm", &shape)?;
1725    match dtype {
1726        DType::F32 => TracedTensor::from_vec_col_major(shape, vec![0.0_f32; len]),
1727        DType::F64 => TracedTensor::from_vec_col_major(shape, vec![0.0_f64; len]),
1728        DType::C32 => TracedTensor::from_vec_col_major(shape, vec![Complex32::new(0.0, 0.0); len]),
1729        DType::C64 => TracedTensor::from_vec_col_major(shape, vec![Complex64::new(0.0, 0.0); len]),
1730        _ => Err(Error::TensorRuntime(
1731            tenferro_tensor::Error::unsupported_dtype(
1732                "norm",
1733                dtype,
1734                "norm supports only floating-point and complex dtypes",
1735            ),
1736        )),
1737    }
1738}
1739
1740fn checked_element_count(op: &'static str, shape: &[usize]) -> Result<usize> {
1741    shape.iter().try_fold(1usize, |acc, &dim| {
1742        acc.checked_mul(dim).ok_or_else(|| {
1743            Error::TensorRuntime(tenferro_tensor::Error::invalid_argument(
1744                op,
1745                "shape",
1746                "shape element count overflow",
1747            ))
1748        })
1749    })
1750}
1751
1752fn default_pinv_rtol(dtype: DType, max_dim: usize) -> f64 {
1753    let eps = match dtype {
1754        DType::F32 | DType::C32 => f32::EPSILON as f64,
1755        DType::F64 | DType::C64 => f64::EPSILON,
1756        DType::I32 | DType::I64 | DType::Bool => 0.0,
1757    };
1758    eps * max_dim as f64
1759}
1760
1761fn vector_norm(a: &TracedTensor, axis: usize, ord: Option<f64>) -> Result<TracedTensor> {
1762    let abs = a.abs()?;
1763    match ord {
1764        None => frobenius_norm(&abs, &[axis]),
1765        Some(0.0) => count_nonzero(&abs, &[axis]),
1766        Some(p) if p == f64::INFINITY => abs.reduce_max(Some(&[axis])),
1767        Some(p) if p == f64::NEG_INFINITY => abs.reduce_min(Some(&[axis])),
1768        Some(p) => p_norm(&abs, &[axis], p),
1769    }
1770}
1771
1772fn matrix_norm(a: &TracedTensor, axes: &[usize], ord: Option<f64>) -> Result<TracedTensor> {
1773    let matrix = move_axes_to_front(a, axes)?;
1774    let abs = matrix.abs()?;
1775    match ord {
1776        None => frobenius_norm(&abs, &[0, 1]),
1777        Some(p) if p == f64::INFINITY => matrix_row_sum_norm(&abs, true),
1778        Some(p) if p == f64::NEG_INFINITY => matrix_row_sum_norm(&abs, false),
1779        Some(1.0) => matrix_col_sum_norm(&abs, true),
1780        Some(-1.0) => matrix_col_sum_norm(&abs, false),
1781        Some(2.0) => {
1782            let singular_values = svd_values(&matrix)?.abs()?;
1783            singular_values.reduce_max(Some(&[0]))
1784        }
1785        Some(-2.0) => {
1786            let singular_values = svd_values(&matrix)?.abs()?;
1787            singular_values.reduce_min(Some(&[0]))
1788        }
1789        Some(0.0) => count_nonzero(&abs, &[0, 1]),
1790        Some(p) => p_norm(&abs, &[0, 1], p),
1791    }
1792}
1793
1794fn svd_values(a: &TracedTensor) -> Result<TracedTensor> {
1795    let (_u, s, _vt) = three_outputs(
1796        apply(
1797            Arc::new(LinalgExtensionOp::new(LinalgOp::Svd {
1798                derivative_eps: SvdOptions::default().derivative_eps,
1799                gauge: SvdOptions::default().gauge,
1800            })),
1801            &[a],
1802        )?,
1803        "svd_values",
1804    )?;
1805    Ok(s)
1806}
1807
1808fn eigh_values(a: &TracedTensor) -> Result<TracedTensor> {
1809    let (values, _vectors) = two_outputs(
1810        apply(
1811            Arc::new(LinalgExtensionOp::new(LinalgOp::Eigh {
1812                derivative_eps: EighOptions::default().derivative_eps,
1813                gauge: EighOptions::default().gauge,
1814            })),
1815            &[a],
1816        )?,
1817        "eigh_values",
1818    )?;
1819    Ok(values)
1820}
1821
1822fn eig_values(a: &TracedTensor) -> Result<TracedTensor> {
1823    let (values, _vectors) = two_outputs(
1824        apply(
1825            Arc::new(LinalgExtensionOp::new(LinalgOp::Eig {
1826                input_dtype: a.dtype,
1827            })),
1828            &[a],
1829        )?,
1830        "eig_values",
1831    )?;
1832    Ok(values)
1833}
1834
1835fn scale_matrix_columns(matrix: &TracedTensor, scale: &TracedTensor) -> Result<TracedTensor> {
1836    let matrix_shape = matrix.concrete_shape()?;
1837    let scale_shape_input = scale.concrete_shape()?;
1838    let mut scale_shape = vec![1, scale_shape_input[0]];
1839    scale_shape.extend_from_slice(&matrix_shape[2..]);
1840    let dims: Vec<usize> = (0..matrix_shape.len()).collect();
1841    let scale = scale
1842        .reshape(&scale_shape)?
1843        .broadcast_in_dim(&matrix_shape, &dims)?;
1844    matrix * &scale
1845}
1846
1847fn count_nonzero(abs: &TracedTensor, axes: &[usize]) -> Result<TracedTensor> {
1848    let mask = abs.compare(&zero_scalar(abs.dtype)?, CompareDir::Gt)?;
1849    mask.convert(abs.dtype)?.reduce_sum(Some(axes))
1850}
1851
1852fn matrix_row_sum_norm(abs: &TracedTensor, take_max: bool) -> Result<TracedTensor> {
1853    let row_sums = abs.reduce_sum(Some(&[1]))?;
1854    if take_max {
1855        row_sums.reduce_max(Some(&[0]))
1856    } else {
1857        row_sums.reduce_min(Some(&[0]))
1858    }
1859}
1860
1861fn matrix_col_sum_norm(abs: &TracedTensor, take_max: bool) -> Result<TracedTensor> {
1862    let col_sums = abs.reduce_sum(Some(&[0]))?;
1863    if take_max {
1864        col_sums.reduce_max(Some(&[0]))
1865    } else {
1866        col_sums.reduce_min(Some(&[0]))
1867    }
1868}
1869
1870fn move_axes_to_front(tensor: &TracedTensor, axes: &[usize]) -> Result<TracedTensor> {
1871    if axes.iter().enumerate().all(|(index, &axis)| index == axis) {
1872        return Ok(tensor.clone());
1873    }
1874
1875    let mut selected = vec![false; tensor.rank];
1876    for &axis in axes {
1877        selected[axis] = true;
1878    }
1879
1880    let mut perm = Vec::with_capacity(tensor.rank);
1881    perm.extend_from_slice(axes);
1882    for (axis, is_selected) in selected.iter().enumerate().take(tensor.rank) {
1883        if !*is_selected {
1884            perm.push(axis);
1885        }
1886    }
1887    tensor.transpose(&perm)
1888}
1889
1890fn restore_keepdim(
1891    reduced: TracedTensor,
1892    original_shape: &[usize],
1893    axes: &[usize],
1894    keepdim: bool,
1895) -> Result<TracedTensor> {
1896    if !keepdim {
1897        return Ok(reduced);
1898    }
1899    let mut kept_shape = original_shape.to_vec();
1900    for &axis in axes {
1901        kept_shape[axis] = 1;
1902    }
1903    reduced.reshape(&kept_shape)
1904}
1905
1906#[cfg(test)]
1907mod tests {
1908    use super::p_norm;
1909    use tenferro_runtime::TracedTensor;
1910
1911    #[test]
1912    fn p_norm_rejects_zero_and_non_finite_orders() {
1913        let x = TracedTensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap();
1914        let abs = x.abs().unwrap();
1915
1916        for p in [0.0, f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
1917            let err = p_norm(&abs, &[0], p).unwrap_err();
1918            assert!(
1919                err.to_string().contains("finite") || err.to_string().contains("nonzero"),
1920                "expected finite nonzero order error, got {err:?}"
1921            );
1922        }
1923    }
1924}