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