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