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