Skip to main content

tenferro_linalg/
eager_ext.rs

1use std::sync::{Arc, OnceLock};
2
3use tenferro_ad::error::{Error, Result};
4use tenferro_ad::extension::apply_eager_with_extension_session;
5use tenferro_ad::EagerTensor;
6use tenferro_runtime::{ErrorPhase, ExtensionModule};
7
8use crate::eager_composites;
9use crate::extension::{
10    extension_module, validate_derivative_eps, EighOptions, LinalgExtensionOp, LinalgOp, QrOptions,
11    SvdOptions,
12};
13
14/// Linear algebra extension methods for [`EagerTensor`].
15pub trait EagerTensorLinalgExt {
16    /// # Errors
17    ///
18    /// Returns `Error::Validation` for an invalid rank, shape, or dtype,
19    /// `Error::Extension` with an unsupported-operation or unsupported-dtype
20    /// source when the selected backend cannot execute the decomposition, and
21    /// `Error::RuntimeState` when the eager runtime or backend is unavailable.
22    /// # Examples
23    ///
24    /// ```rust
25    /// # use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
26    /// # use tenferro_cpu::CpuBackend;
27    /// # use tenferro_linalg::EagerTensorLinalgExt;
28    /// # let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
29    /// # let a = EagerTensor::from_tensor_in(
30    /// #     Tensor::from_vec_col_major(vec![2, 2], vec![2.0_f64, 0.0, 0.0, 4.0]).unwrap(),
31    /// #     ctx,
32    /// # )?;
33    /// let (_u, s, _vt) = a.svd()?;
34    /// assert_eq!(s.shape(), &[2]);
35    /// # Ok::<(), tenferro_ad::Error>(())
36    /// ```
37    fn svd(&self) -> Result<(EagerTensor, EagerTensor, EagerTensor)>;
38    /// # Errors
39    ///
40    /// Returns `Error::Validation` when `derivative_eps` is non-finite or
41    /// non-positive, `Error::Extension` for unsupported dtypes or numerical
42    /// non-convergence, and `Error::Internal` if the extension violates its
43    /// output-count contract.
44    /// # Examples
45    ///
46    /// ```rust
47    /// # use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
48    /// # use tenferro_cpu::CpuBackend;
49    /// # use tenferro_linalg::{EagerTensorLinalgExt, SvdOptions};
50    /// # let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
51    /// # let a = EagerTensor::from_tensor_in(
52    /// #     Tensor::from_vec_col_major(vec![2, 2], vec![2.0_f64, 0.0, 0.0, 4.0]).unwrap(),
53    /// #     ctx,
54    /// # )?;
55    /// let (_u, s, _vt) = a.svd_with_options(SvdOptions::default())?;
56    /// assert_eq!(s.shape(), &[2]);
57    /// # Ok::<(), tenferro_ad::Error>(())
58    /// ```
59    fn svd_with_options(
60        &self,
61        options: SvdOptions,
62    ) -> Result<(EagerTensor, EagerTensor, EagerTensor)>;
63    /// Full-matrices SVD returning square `U (m x m)` and `Vh (n x n)`, whose
64    /// trailing `n - rank` rows span the input's right nullspace.
65    ///
66    /// # Errors
67    ///
68    /// Returns `Error::Validation` for an invalid rank, and `Error::Extension`
69    /// with an unsupported-operation source when the active backend does not
70    /// implement full-matrices SVD (the CPU faer provider supports it; the
71    /// LAPACK provider and GPU backends return an unsupported error in this
72    /// slice). Automatic differentiation through the full variant is
73    /// unsupported and surfaces a typed error rather than a silent thin
74    /// fallback.
75    /// # Examples
76    ///
77    /// ```rust
78    /// # #[cfg(feature = "cpu-faer")]
79    /// # {
80    /// # use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
81    /// # use tenferro_cpu::{CpuBackend, CpuBackendKind};
82    /// # use tenferro_linalg::EagerTensorLinalgExt;
83    /// # let faer = CpuBackend::with_threads_and_kind(1, CpuBackendKind::Faer)
84    /// #     .expect("faer CPU backend");
85    /// # let ctx = EagerRuntime::with_cpu_backend(faer)?;
86    /// # let a = EagerTensor::from_tensor_in(
87    /// #     Tensor::from_vec_col_major(vec![1, 2], vec![1.0_f64, 1.0]).unwrap(),
88    /// #     ctx,
89    /// # )?;
90    /// let (u, s, vh) = a.svd_full()?;
91    /// assert_eq!(u.shape(), &[1, 1]);
92    /// assert_eq!(s.shape(), &[1]);
93    /// assert_eq!(vh.shape(), &[2, 2]);
94    /// let singular_values = s.value()?.as_slice::<f64>()?;
95    /// assert!((singular_values[0] - 2.0_f64.sqrt()).abs() < 1e-12);
96    /// # }
97    /// # #[cfg(all(feature = "cpu-blas", not(feature = "cpu-faer")))]
98    /// # {
99    /// # use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
100    /// # use tenferro_cpu::{CpuBackend, CpuBackendKind};
101    /// # use tenferro_linalg::EagerTensorLinalgExt;
102    /// # let blas = CpuBackend::with_threads_and_kind(1, CpuBackendKind::Blas)
103    /// #     .expect("BLAS CPU backend");
104    /// # let ctx = EagerRuntime::with_cpu_backend(blas)?;
105    /// # let a = EagerTensor::from_tensor_in(
106    /// #     Tensor::from_vec_col_major(vec![1, 2], vec![1.0_f64, 1.0]).unwrap(),
107    /// #     ctx,
108    /// # )?;
109    /// # let error = match a.svd_full() {
110    /// #     Ok(_) => panic!("expected full-matrices SVD to be unsupported for BLAS"),
111    /// #     Err(error) => error,
112    /// # };
113    /// # assert!(error.to_string().contains("full-matrices SVD"));
114    /// # }
115    /// # Ok::<(), tenferro_ad::Error>(())
116    /// ```
117    fn svd_full(&self) -> Result<(EagerTensor, EagerTensor, EagerTensor)>;
118    /// # Errors
119    ///
120    /// Returns `Error::Validation` for invalid matrix rank or shape,
121    /// `Error::Extension` for unsupported dtypes or numerical failure, and
122    /// `Error::RuntimeState` when the eager runtime or backend is unavailable.
123    /// # Examples
124    ///
125    /// ```rust
126    /// # use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
127    /// # use tenferro_cpu::CpuBackend;
128    /// # use tenferro_linalg::EagerTensorLinalgExt;
129    /// # let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
130    /// # let a = EagerTensor::from_tensor_in(
131    /// #     Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 0.0, 0.0, 1.0]).unwrap(),
132    /// #     ctx,
133    /// # )?;
134    /// let (q, r) = a.qr()?;
135    /// assert_eq!(q.shape(), &[2, 2]);
136    /// assert_eq!(r.shape(), &[2, 2]);
137    /// # Ok::<(), tenferro_ad::Error>(())
138    /// ```
139    fn qr(&self) -> Result<(EagerTensor, EagerTensor)>;
140    /// # Errors
141    ///
142    /// Returns `Error::Validation` for invalid matrix rank or shape,
143    /// `Error::Extension` for unsupported dtypes or numerical failure, and
144    /// `Error::Internal` if the extension violates its output-count contract.
145    /// # Examples
146    ///
147    /// ```rust
148    /// # use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
149    /// # use tenferro_cpu::CpuBackend;
150    /// # use tenferro_linalg::{EagerTensorLinalgExt, QrOptions};
151    /// # let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
152    /// # let a = EagerTensor::from_tensor_in(
153    /// #     Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 0.0, 0.0, 1.0]).unwrap(),
154    /// #     ctx,
155    /// # )?;
156    /// let (q, r) = a.qr_with_options(QrOptions::default())?;
157    /// assert_eq!(q.shape(), &[2, 2]);
158    /// assert_eq!(r.shape(), &[2, 2]);
159    /// # Ok::<(), tenferro_ad::Error>(())
160    /// ```
161    fn qr_with_options(&self, options: QrOptions) -> Result<(EagerTensor, EagerTensor)>;
162    /// # Errors
163    ///
164    /// Returns `Error::Validation` for an invalid matrix rank or shape,
165    /// `Error::Extension` for an unsupported dtype or singular numerical
166    /// result, and `Error::RuntimeState` when execution cannot access its
167    /// backend.
168    /// # Examples
169    ///
170    /// ```rust
171    /// # use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
172    /// # use tenferro_cpu::CpuBackend;
173    /// # use tenferro_linalg::EagerTensorLinalgExt;
174    /// # let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
175    /// # let a = EagerTensor::from_tensor_in(
176    /// #     Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 3.0, 2.0, 4.0]).unwrap(),
177    /// #     ctx,
178    /// # )?;
179    /// let (_p, l, u, parity) = a.lu()?;
180    /// assert_eq!(l.shape(), &[2, 2]);
181    /// assert_eq!(u.shape(), &[2, 2]);
182    /// assert_eq!(parity.shape(), &[]);
183    /// # Ok::<(), tenferro_ad::Error>(())
184    /// ```
185    fn lu(&self) -> Result<(EagerTensor, EagerTensor, EagerTensor, EagerTensor)>;
186    /// # Errors
187    ///
188    /// Returns `Error::Validation` for an invalid matrix rank or shape,
189    /// `Error::Extension` for unsupported dtypes or singular numerical
190    /// results, and `Error::Internal` if the extension violates its output
191    /// contract.
192    /// # Examples
193    ///
194    /// ```rust
195    /// # use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
196    /// # use tenferro_cpu::CpuBackend;
197    /// # use tenferro_linalg::EagerTensorLinalgExt;
198    /// # let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
199    /// # let a = EagerTensor::from_tensor_in(
200    /// #     Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 3.0, 2.0, 4.0]).unwrap(),
201    /// #     ctx,
202    /// # )?;
203    /// let (p, _l, _u, q, parity) = a.full_piv_lu()?;
204    /// assert_eq!(p.shape(), &[2, 2]);
205    /// assert_eq!(q.shape(), &[2, 2]);
206    /// assert_eq!(parity.shape(), &[]);
207    /// # Ok::<(), tenferro_ad::Error>(())
208    /// ```
209    fn full_piv_lu(
210        &self,
211    ) -> Result<(
212        EagerTensor,
213        EagerTensor,
214        EagerTensor,
215        EagerTensor,
216        EagerTensor,
217    )>;
218    /// # Errors
219    ///
220    /// Returns `Error::Validation` when `a` and `b` have incompatible matrix
221    /// or batch shapes, `Error::Extension` for an unsupported dtype or
222    /// singular system, and `Error::RuntimeState` when the backend is
223    /// unavailable.
224    /// # Examples
225    ///
226    /// ```rust
227    /// # use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
228    /// # use tenferro_cpu::CpuBackend;
229    /// # use tenferro_linalg::EagerTensorLinalgExt;
230    /// # let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
231    /// # let a = EagerTensor::from_tensor_in(
232    /// #     Tensor::from_vec_col_major(vec![2, 2], vec![0.0_f64, 2.0, 1.0, 3.0]).unwrap(),
233    /// #     ctx.clone(),
234    /// # )?;
235    /// # let b = EagerTensor::from_tensor_in(
236    /// #     Tensor::from_vec_col_major(vec![2, 1], vec![-1.0_f64, 5.0]).unwrap(),
237    /// #     ctx,
238    /// # )?;
239    /// let x = a.full_piv_lu_solve(&b)?;
240    /// assert_eq!(x.shape(), &[2, 1]);
241    /// # Ok::<(), tenferro_ad::Error>(())
242    /// ```
243    fn full_piv_lu_solve(&self, b: &EagerTensor) -> Result<EagerTensor>;
244    /// # Errors
245    ///
246    /// Returns `Error::Validation` for incompatible matrix, batch, or dtype
247    /// metadata, `Error::Extension` for an unsupported dtype or singular
248    /// system, and `Error::RuntimeState` when the backend is unavailable.
249    /// # Examples
250    ///
251    /// ```rust
252    /// # use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
253    /// # use tenferro_cpu::CpuBackend;
254    /// # use tenferro_linalg::EagerTensorLinalgExt;
255    /// # let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
256    /// # let a = EagerTensor::from_tensor_in(
257    /// #     Tensor::from_vec_col_major(vec![2, 2], vec![2.0_f64, 0.0, 0.0, 4.0]).unwrap(),
258    /// #     ctx.clone(),
259    /// # )?;
260    /// # let b = EagerTensor::from_tensor_in(
261    /// #     Tensor::from_vec_col_major(vec![2, 1], vec![4.0_f64, 8.0]).unwrap(),
262    /// #     ctx,
263    /// # )?;
264    /// let x = a.solve(&b)?;
265    /// assert_eq!(x.value()?.as_slice::<f64>()?, &[2.0, 2.0]);
266    /// # Ok::<(), tenferro_ad::Error>(())
267    /// ```
268    fn solve(&self, b: &EagerTensor) -> Result<EagerTensor>;
269    /// Least-squares solve `argmin_x ||A x - b||_2` for a tall or square,
270    /// full-column-rank `A`, via the thin QR factorization.
271    ///
272    /// # Errors
273    ///
274    /// Returns `Error::Validation` when `A` or `b` is not a matrix (rank
275    /// `>= 2`), when `A` is wide (`rows < cols`; underdetermined), or when the
276    /// dtype is not floating-point or complex; `Error::Extension` for backend
277    /// QR or triangular-solve failures; and `Error::RuntimeState` when the
278    /// backend is unavailable. Rank-deficient `A` is not detected and yields an
279    /// ill-defined result.
280    /// # Examples
281    ///
282    /// ```rust
283    /// # use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
284    /// # use tenferro_cpu::CpuBackend;
285    /// # use tenferro_linalg::EagerTensorLinalgExt;
286    /// # let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
287    /// # let a = EagerTensor::from_tensor_in(
288    /// #     Tensor::from_vec_col_major(
289    /// #         vec![3, 2],
290    /// #         vec![1.0_f64, 0.0, 1.0, 0.0, 1.0, 1.0],
291    /// #     )
292    /// #     .unwrap(),
293    /// #     ctx.clone(),
294    /// # )?;
295    /// # let b = EagerTensor::from_tensor_in(
296    /// #     Tensor::from_vec_col_major(vec![3, 1], vec![1.0_f64, 2.0, 3.0]).unwrap(),
297    /// #     ctx,
298    /// # )?;
299    /// let x = a.lstsq(&b)?;
300    /// assert_eq!(x.shape(), &[2, 1]);
301    /// let values = x.value()?.as_slice::<f64>()?;
302    /// assert!((values[0] - 1.0_f64).abs() < 1e-12);
303    /// assert!((values[1] - 2.0_f64).abs() < 1e-12);
304    /// # Ok::<(), tenferro_ad::Error>(())
305    /// ```
306    fn lstsq(&self, b: &EagerTensor) -> Result<EagerTensor>;
307    /// # Errors
308    ///
309    /// Returns `Error::Validation` for a non-square or invalid-rank input,
310    /// `Error::Extension` for unsupported dtypes or a non-positive-definite
311    /// matrix, and `Error::RuntimeState` when the backend is unavailable.
312    /// # Examples
313    ///
314    /// ```rust
315    /// # use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
316    /// # use tenferro_cpu::CpuBackend;
317    /// # use tenferro_linalg::EagerTensorLinalgExt;
318    /// # let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
319    /// # let a = EagerTensor::from_tensor_in(
320    /// #     Tensor::from_vec_col_major(vec![2, 2], vec![4.0_f64, 2.0, 2.0, 3.0]).unwrap(),
321    /// #     ctx,
322    /// # )?;
323    /// let l = a.cholesky()?;
324    /// assert_eq!(l.shape(), &[2, 2]);
325    /// # Ok::<(), tenferro_ad::Error>(())
326    /// ```
327    fn cholesky(&self) -> Result<EagerTensor>;
328    /// # Errors
329    ///
330    /// Returns `Error::Validation` for a non-square or invalid-rank input,
331    /// `Error::Extension` for unsupported dtypes or numerical non-convergence,
332    /// and `Error::RuntimeState` when the backend is unavailable.
333    /// # Examples
334    ///
335    /// ```rust
336    /// # use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
337    /// # use tenferro_cpu::CpuBackend;
338    /// # use tenferro_linalg::EagerTensorLinalgExt;
339    /// # let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
340    /// # let a = EagerTensor::from_tensor_in(
341    /// #     Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 0.0, 0.0, 3.0]).unwrap(),
342    /// #     ctx,
343    /// # )?;
344    /// let (values, vectors) = a.eigh()?;
345    /// assert_eq!(values.value()?.as_slice::<f64>()?, &[1.0, 3.0]);
346    /// assert_eq!(vectors.shape(), &[2, 2]);
347    /// # Ok::<(), tenferro_ad::Error>(())
348    /// ```
349    fn eigh(&self) -> Result<(EagerTensor, EagerTensor)>;
350    /// # Errors
351    ///
352    /// Returns `Error::Validation` for an invalid rank, shape, or
353    /// `derivative_eps`, `Error::Extension` for unsupported dtypes or
354    /// non-convergence, and `Error::Internal` for an output-count violation.
355    /// # Examples
356    ///
357    /// ```rust
358    /// # use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
359    /// # use tenferro_cpu::CpuBackend;
360    /// # use tenferro_linalg::{EagerTensorLinalgExt, EighOptions};
361    /// # let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
362    /// # let a = EagerTensor::from_tensor_in(
363    /// #     Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 0.0, 0.0, 3.0]).unwrap(),
364    /// #     ctx,
365    /// # )?;
366    /// let (values, vectors) = a.eigh_with_options(EighOptions::default())?;
367    /// assert_eq!(values.shape(), &[2]);
368    /// assert_eq!(vectors.shape(), &[2, 2]);
369    /// # Ok::<(), tenferro_ad::Error>(())
370    /// ```
371    fn eigh_with_options(&self, options: EighOptions) -> Result<(EagerTensor, EagerTensor)>;
372    /// # Errors
373    ///
374    /// Returns `Error::Validation` for a non-square or invalid-rank input,
375    /// `Error::Extension` for unsupported dtypes or numerical non-convergence,
376    /// and `Error::RuntimeState` when the backend is unavailable.
377    /// # Examples
378    ///
379    /// ```rust
380    /// # use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
381    /// # use tenferro_cpu::CpuBackend;
382    /// # use tenferro_linalg::EagerTensorLinalgExt;
383    /// # let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
384    /// # let a = EagerTensor::from_tensor_in(
385    /// #     Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 0.0, 0.0, 2.0]).unwrap(),
386    /// #     ctx,
387    /// # )?;
388    /// let (values, vectors) = a.eig()?;
389    /// assert_eq!(values.shape(), &[2]);
390    /// assert_eq!(vectors.shape(), &[2, 2]);
391    /// # Ok::<(), tenferro_ad::Error>(())
392    /// ```
393    fn eig(&self) -> Result<(EagerTensor, EagerTensor)>;
394    /// # Errors
395    ///
396    /// Returns `Error::Validation` for incompatible matrix, batch, or dtype
397    /// metadata, `Error::Extension` for unsupported dtypes or a singular
398    /// system, and `Error::RuntimeState` when the backend is unavailable.
399    /// # Examples
400    ///
401    /// ```rust
402    /// # use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
403    /// # use tenferro_cpu::CpuBackend;
404    /// # use tenferro_linalg::EagerTensorLinalgExt;
405    /// # let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
406    /// # let a = EagerTensor::from_tensor_in(
407    /// #     Tensor::from_vec_col_major(vec![2, 2], vec![2.0_f64, 0.0, 1.0, 3.0]).unwrap(),
408    /// #     ctx.clone(),
409    /// # )?;
410    /// # let b = EagerTensor::from_tensor_in(
411    /// #     Tensor::from_vec_col_major(vec![2, 1], vec![4.0_f64, 9.0]).unwrap(),
412    /// #     ctx,
413    /// # )?;
414    /// let x = a.triangular_solve(&b, true, false, false, false)?;
415    /// assert_eq!(x.shape(), &[2, 1]);
416    /// # Ok::<(), tenferro_ad::Error>(())
417    /// ```
418    fn triangular_solve(
419        &self,
420        b: &EagerTensor,
421        left_side: bool,
422        lower: bool,
423        transpose_a: bool,
424        unit_diagonal: bool,
425    ) -> Result<EagerTensor>;
426    /// Return the determinant sign and logarithm of its absolute value.
427    ///
428    /// # Errors
429    ///
430    /// Returns `Error::Validation` for a non-square input, `Error::Extension`
431    /// for an unsupported dtype or numerical failure, and `Error::Internal`
432    /// if a primitive violates its output contract.
433    /// # Examples
434    ///
435    /// ```rust
436    /// # use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
437    /// # use tenferro_cpu::CpuBackend;
438    /// # use tenferro_linalg::EagerTensorLinalgExt;
439    /// # let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
440    /// # let a = EagerTensor::from_tensor_in(
441    /// #     Tensor::from_vec_col_major(vec![2, 2], vec![2.0_f64, 0.0, 0.0, 4.0]).unwrap(),
442    /// #     ctx,
443    /// # )?;
444    /// let (sign, logabsdet) = a.slogdet()?;
445    /// assert_eq!(sign.value()?.as_slice::<f64>()?, &[1.0]);
446    /// assert_eq!(logabsdet.shape(), &[]);
447    /// # Ok::<(), tenferro_ad::Error>(())
448    /// ```
449    fn slogdet(&self) -> Result<(EagerTensor, EagerTensor)>;
450    /// Return the determinant.
451    ///
452    /// # Errors
453    ///
454    /// Returns `Error::Validation`, `Error::Extension`, `Error::RuntimeState`,
455    /// or `Error::Internal` under the conditions documented by [`Self::slogdet`].
456    /// # Examples
457    ///
458    /// ```rust
459    /// # use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
460    /// # use tenferro_cpu::CpuBackend;
461    /// # use tenferro_linalg::EagerTensorLinalgExt;
462    /// # let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
463    /// # let a = EagerTensor::from_tensor_in(
464    /// #     Tensor::from_vec_col_major(vec![2, 2], vec![2.0_f64, 0.0, 0.0, 4.0]).unwrap(),
465    /// #     ctx,
466    /// # )?;
467    /// let determinant = a.det()?;
468    /// assert!((determinant.value()?.as_slice::<f64>()?[0] - 8.0).abs() < 1.0e-12);
469    /// # Ok::<(), tenferro_ad::Error>(())
470    /// ```
471    fn det(&self) -> Result<EagerTensor>;
472    /// Return the matrix inverse.
473    ///
474    /// # Errors
475    ///
476    /// Returns `Error::Validation` for invalid matrix metadata,
477    /// `Error::Extension` for an unsupported dtype or singular solve, and
478    /// `Error::RuntimeState` when eager execution cannot access its backend.
479    /// # Examples
480    ///
481    /// ```rust
482    /// # use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
483    /// # use tenferro_cpu::CpuBackend;
484    /// # use tenferro_linalg::EagerTensorLinalgExt;
485    /// # let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
486    /// # let a = EagerTensor::from_tensor_in(
487    /// #     Tensor::from_vec_col_major(vec![2, 2], vec![2.0_f64, 0.0, 0.0, 4.0]).unwrap(),
488    /// #     ctx,
489    /// # )?;
490    /// let inverse = a.inv()?;
491    /// assert_eq!(inverse.value()?.as_slice::<f64>()?, &[0.5, 0.0, 0.0, 0.25]);
492    /// # Ok::<(), tenferro_ad::Error>(())
493    /// ```
494    fn inv(&self) -> Result<EagerTensor>;
495    /// Return Hermitian eigenvalues without eigenvectors.
496    ///
497    /// # Errors
498    ///
499    /// Returns the validation, unsupported-dtype, numerical-convergence,
500    /// runtime-state, or output-contract errors reported by [`Self::eigh`].
501    /// # Examples
502    ///
503    /// ```rust
504    /// # use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
505    /// # use tenferro_cpu::CpuBackend;
506    /// # use tenferro_linalg::EagerTensorLinalgExt;
507    /// # let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
508    /// # let a = EagerTensor::from_tensor_in(
509    /// #     Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 0.0, 0.0, 3.0]).unwrap(),
510    /// #     ctx,
511    /// # )?;
512    /// let values = a.eigvalsh()?;
513    /// assert_eq!(values.value()?.as_slice::<f64>()?, &[1.0, 3.0]);
514    /// # Ok::<(), tenferro_ad::Error>(())
515    /// ```
516    fn eigvalsh(&self) -> Result<EagerTensor>;
517    /// Return general eigenvalues without eigenvectors.
518    ///
519    /// # Errors
520    ///
521    /// Returns the validation, unsupported-dtype, numerical-convergence,
522    /// runtime-state, or output-contract errors reported by [`Self::eig`].
523    /// # Examples
524    ///
525    /// ```rust
526    /// # use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
527    /// # use tenferro_cpu::CpuBackend;
528    /// # use tenferro_linalg::EagerTensorLinalgExt;
529    /// # let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
530    /// # let a = EagerTensor::from_tensor_in(
531    /// #     Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 0.0, 0.0, 2.0]).unwrap(),
532    /// #     ctx,
533    /// # )?;
534    /// let values = a.eigvals()?;
535    /// assert_eq!(values.shape(), &[2]);
536    /// # Ok::<(), tenferro_ad::Error>(())
537    /// ```
538    fn eigvals(&self) -> Result<EagerTensor>;
539    /// Return the Moore-Penrose pseudoinverse with the default tolerance.
540    ///
541    /// # Errors
542    ///
543    /// Returns `Error::Validation` for invalid matrix metadata,
544    /// `Error::Extension` for unsupported SVD execution, and
545    /// `Error::Internal` for an unexpected decomposition output contract.
546    /// # Examples
547    ///
548    /// ```rust
549    /// # use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
550    /// # use tenferro_cpu::CpuBackend;
551    /// # use tenferro_linalg::EagerTensorLinalgExt;
552    /// # let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
553    /// # let a = EagerTensor::from_tensor_in(
554    /// #     Tensor::from_vec_col_major(vec![2, 2], vec![2.0_f64, 0.0, 0.0, 4.0]).unwrap(),
555    /// #     ctx,
556    /// # )?;
557    /// let pseudoinverse = a.pinv()?;
558    /// assert_eq!(pseudoinverse.shape(), &[2, 2]);
559    /// # Ok::<(), tenferro_ad::Error>(())
560    /// ```
561    fn pinv(&self) -> Result<EagerTensor>;
562    /// Return the Moore-Penrose pseudoinverse with an explicit relative tolerance.
563    ///
564    /// # Errors
565    ///
566    /// Returns `Error::Validation` when `rtol` is negative or non-finite, plus
567    /// the `Error::Extension` and output-contract errors reported by [`Self::pinv`].
568    /// # Examples
569    ///
570    /// ```rust
571    /// # use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
572    /// # use tenferro_cpu::CpuBackend;
573    /// # use tenferro_linalg::EagerTensorLinalgExt;
574    /// # let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
575    /// # let a = EagerTensor::from_tensor_in(
576    /// #     Tensor::from_vec_col_major(vec![2, 2], vec![2.0_f64, 0.0, 0.0, 4.0]).unwrap(),
577    /// #     ctx,
578    /// # )?;
579    /// let pseudoinverse = a.pinv_with_rtol(1.0e-12)?;
580    /// assert_eq!(pseudoinverse.shape(), &[2, 2]);
581    /// # Ok::<(), tenferro_ad::Error>(())
582    /// ```
583    fn pinv_with_rtol(&self, rtol: f64) -> Result<EagerTensor>;
584    /// Return a vector, matrix, or tensor norm.
585    ///
586    /// # Errors
587    ///
588    /// Returns `Error::Validation` for an unsupported order/dimension
589    /// combination or invalid axes, `Error::Extension` when a required matrix
590    /// decomposition is unsupported, and eager runtime/backend failures.
591    /// # Examples
592    ///
593    /// ```rust
594    /// # use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
595    /// # use tenferro_cpu::CpuBackend;
596    /// # use tenferro_linalg::EagerTensorLinalgExt;
597    /// # let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
598    /// # let a = EagerTensor::from_tensor_in(
599    /// #     Tensor::from_vec_col_major(vec![2, 2], vec![3.0_f64, 0.0, 0.0, 4.0]).unwrap(),
600    /// #     ctx,
601    /// # )?;
602    /// let frobenius = a.norm(None, None, false)?;
603    /// assert_eq!(frobenius.shape(), &[]);
604    /// # Ok::<(), tenferro_ad::Error>(())
605    /// ```
606    fn norm(&self, ord: Option<f64>, dim: Option<&[usize]>, keepdim: bool) -> Result<EagerTensor>;
607}
608
609impl EagerTensorLinalgExt for EagerTensor {
610    fn svd(&self) -> Result<(EagerTensor, EagerTensor, EagerTensor)> {
611        svd(self)
612    }
613
614    fn svd_with_options(
615        &self,
616        options: SvdOptions,
617    ) -> Result<(EagerTensor, EagerTensor, EagerTensor)> {
618        svd_with_options(self, options)
619    }
620
621    fn svd_full(&self) -> Result<(EagerTensor, EagerTensor, EagerTensor)> {
622        svd_full(self)
623    }
624
625    fn qr(&self) -> Result<(EagerTensor, EagerTensor)> {
626        qr(self)
627    }
628
629    fn qr_with_options(&self, options: QrOptions) -> Result<(EagerTensor, EagerTensor)> {
630        qr_with_options(self, options)
631    }
632
633    fn lu(&self) -> Result<(EagerTensor, EagerTensor, EagerTensor, EagerTensor)> {
634        lu(self)
635    }
636
637    fn full_piv_lu(
638        &self,
639    ) -> Result<(
640        EagerTensor,
641        EagerTensor,
642        EagerTensor,
643        EagerTensor,
644        EagerTensor,
645    )> {
646        full_piv_lu(self)
647    }
648
649    fn full_piv_lu_solve(&self, b: &EagerTensor) -> Result<EagerTensor> {
650        full_piv_lu_solve(self, b)
651    }
652
653    fn solve(&self, b: &EagerTensor) -> Result<EagerTensor> {
654        solve(self, b)
655    }
656
657    fn lstsq(&self, b: &EagerTensor) -> Result<EagerTensor> {
658        eager_composites::lstsq(self, b)
659    }
660
661    fn cholesky(&self) -> Result<EagerTensor> {
662        cholesky(self)
663    }
664
665    fn eigh(&self) -> Result<(EagerTensor, EagerTensor)> {
666        eigh(self)
667    }
668
669    fn eigh_with_options(&self, options: EighOptions) -> Result<(EagerTensor, EagerTensor)> {
670        eigh_with_options(self, options)
671    }
672
673    fn eig(&self) -> Result<(EagerTensor, EagerTensor)> {
674        eig(self)
675    }
676
677    fn triangular_solve(
678        &self,
679        b: &EagerTensor,
680        left_side: bool,
681        lower: bool,
682        transpose_a: bool,
683        unit_diagonal: bool,
684    ) -> Result<EagerTensor> {
685        triangular_solve(self, b, left_side, lower, transpose_a, unit_diagonal)
686    }
687
688    fn slogdet(&self) -> Result<(EagerTensor, EagerTensor)> {
689        eager_composites::slogdet(self)
690    }
691
692    fn det(&self) -> Result<EagerTensor> {
693        eager_composites::det(self)
694    }
695
696    fn inv(&self) -> Result<EagerTensor> {
697        eager_composites::inv(self)
698    }
699
700    fn eigvalsh(&self) -> Result<EagerTensor> {
701        eager_composites::eigvalsh(self)
702    }
703
704    fn eigvals(&self) -> Result<EagerTensor> {
705        eager_composites::eigvals(self)
706    }
707
708    fn pinv(&self) -> Result<EagerTensor> {
709        eager_composites::pinv(self)
710    }
711
712    fn pinv_with_rtol(&self, rtol: f64) -> Result<EagerTensor> {
713        eager_composites::pinv_with_rtol(self, rtol)
714    }
715
716    fn norm(&self, ord: Option<f64>, dim: Option<&[usize]>, keepdim: bool) -> Result<EagerTensor> {
717        eager_composites::norm(self, ord, dim, keepdim)
718    }
719}
720
721pub(crate) fn apply_linalg_eager(
722    op: LinalgOp,
723    inputs: &[&EagerTensor],
724) -> Result<Vec<EagerTensor>> {
725    let op = Arc::new(LinalgExtensionOp::new(op));
726    apply_eager_with_extension_session(op, inputs, eager_cpu_extension_module()?)
727}
728
729fn eager_cpu_extension_module() -> Result<Arc<dyn ExtensionModule>> {
730    static MODULE: OnceLock<Arc<dyn ExtensionModule>> = OnceLock::new();
731    if let Some(module) = MODULE.get() {
732        return Ok(Arc::clone(module));
733    }
734
735    let engine_id = tenferro_cpu::runtime_engine_id().map_err(eager_runtime_config_error)?;
736    let module = extension_module::<tenferro_cpu::CpuBackend>(engine_id)
737        .map_err(eager_runtime_config_error)?;
738    let _ = MODULE.set(Arc::clone(&module));
739    Ok(MODULE.get().cloned().unwrap_or(module))
740}
741
742fn eager_runtime_config_error(source: tenferro_runtime::RuntimeConfigError) -> Error {
743    Error::runtime_state_source(
744        "tenferro_linalg::eager_extension_module",
745        ErrorPhase::Execution,
746        source,
747    )
748}
749
750/// Singular value decomposition for eager tensors.
751///
752/// # Examples
753///
754/// ```rust
755/// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
756/// use tenferro_linalg::EagerTensorLinalgExt;
757///
758/// let ctx = EagerRuntime::new()?;
759/// let a = EagerTensor::from_tensor_in(
760///     Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 0.0, 0.0, 2.0]).unwrap(),
761///     ctx,
762/// ).unwrap();
763/// let (_u, s, _vt) = a.svd()?;
764/// assert_eq!(s.shape(), &[2]);
765/// # Ok::<(), tenferro_ad::Error>(())
766/// ```
767///
768/// # Errors
769///
770/// Returns `Error::Validation` for an invalid rank, matrix shape, or dtype,
771/// `Error::Extension` with an unsupported-dtype or non-convergence source when
772/// the backend cannot compute the decomposition, and `Error::RuntimeState`
773/// when the eager runtime or backend is unavailable.
774pub fn svd(a: &EagerTensor) -> Result<(EagerTensor, EagerTensor, EagerTensor)> {
775    svd_with_options(a, SvdOptions::default())
776}
777
778/// Singular value decomposition for eager tensors with explicit options.
779///
780/// `derivative_eps` regularizes decomposition derivative formulas. It is not a
781/// backend SVD solver tolerance.
782///
783/// # Examples
784///
785/// ```rust
786/// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
787/// use tenferro_linalg::{EagerTensorLinalgExt, SvdGauge, SvdOptions};
788///
789/// let ctx = EagerRuntime::new()?;
790/// let a = EagerTensor::from_tensor_in(
791///     Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 0.0, 0.0, 2.0]).unwrap(),
792///     ctx,
793/// ).unwrap();
794/// let options = SvdOptions::default()
795///     .gauge(SvdGauge::CanonicalPivot)
796///     .derivative_eps(1.0e-10);
797/// let (_u, s, _vt) = a.svd_with_options(options)?;
798/// assert_eq!(s.shape(), &[2]);
799/// # Ok::<(), tenferro_ad::Error>(())
800/// ```
801///
802/// # Errors
803///
804/// Returns `Error::Validation` when `derivative_eps` is non-finite or
805/// non-positive, `Error::Extension` for unsupported dtypes or numerical
806/// non-convergence, and `Error::Internal` if the extension returns an
807/// unexpected number of outputs.
808pub fn svd_with_options(
809    a: &EagerTensor,
810    options: SvdOptions,
811) -> Result<(EagerTensor, EagerTensor, EagerTensor)> {
812    validate_derivative_eps("svd_with_options", options.derivative_eps)?;
813    let mut outputs = apply_linalg_eager(
814        LinalgOp::Svd {
815            derivative_eps: options.derivative_eps,
816            gauge: options.gauge,
817        },
818        &[a],
819    )?
820    .into_iter();
821    match (
822        outputs.next(),
823        outputs.next(),
824        outputs.next(),
825        outputs.next(),
826    ) {
827        (Some(u), Some(s), Some(vt), None) => Ok((u, s, vt)),
828        _ => Err(Error::Internal(
829            "svd eager op returned an unexpected number of outputs".to_string(),
830        )),
831    }
832}
833
834/// Full-matrices singular value decomposition for eager tensors.
835///
836/// Returns `(U, S, Vh)` with square `U` (`m x m`) and `Vh` (`n x n`); `S` holds
837/// `min(m, n)` singular values. The trailing `n - rank` rows of `Vh` span the
838/// input's right nullspace, which the thin [`svd`] cannot represent.
839///
840/// # Examples
841///
842/// ```rust
843/// # #[cfg(feature = "cpu-faer")]
844/// # {
845/// # use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
846/// # use tenferro_cpu::{CpuBackend, CpuBackendKind};
847/// # use tenferro_linalg::EagerTensorLinalgExt;
848/// # let faer = CpuBackend::with_threads_and_kind(1, CpuBackendKind::Faer)
849/// #     .expect("faer CPU backend");
850/// # let ctx = EagerRuntime::with_cpu_backend(faer)?;
851/// # let a = EagerTensor::from_tensor_in(
852/// #     Tensor::from_vec_col_major(vec![1, 2], vec![1.0_f64, 1.0]).unwrap(),
853/// #     ctx,
854/// # )?;
855/// let (u, s, vh) = a.svd_full()?;
856/// assert_eq!(u.shape(), &[1, 1]);
857/// assert_eq!(s.shape(), &[1]);
858/// assert_eq!(vh.shape(), &[2, 2]);
859/// let singular_values = s.value()?.as_slice::<f64>()?;
860/// assert!((singular_values[0] - 2.0_f64.sqrt()).abs() < 1e-12);
861/// # }
862/// # #[cfg(all(feature = "cpu-blas", not(feature = "cpu-faer")))]
863/// # {
864/// # use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
865/// # use tenferro_cpu::{CpuBackend, CpuBackendKind};
866/// # let blas = CpuBackend::with_threads_and_kind(1, CpuBackendKind::Blas)
867/// #     .expect("BLAS CPU backend");
868/// # let ctx = EagerRuntime::with_cpu_backend(blas)?;
869/// # let a = EagerTensor::from_tensor_in(
870/// #     Tensor::from_vec_col_major(vec![1, 2], vec![1.0_f64, 1.0]).unwrap(),
871/// #     ctx,
872/// # )?;
873/// # use tenferro_linalg::EagerTensorLinalgExt;
874/// # let error = match a.svd_full() {
875/// #     Ok(_) => panic!("expected full-matrices SVD to be unsupported for BLAS"),
876/// #     Err(error) => error,
877/// # };
878/// # assert!(error.to_string().contains("full-matrices SVD"));
879/// # }
880/// # Ok::<(), tenferro_ad::Error>(())
881/// ```
882///
883/// # Errors
884///
885/// Returns `Error::Validation` for an invalid rank and `Error::Extension` with
886/// an unsupported-operation source when the active backend does not implement
887/// full-matrices SVD (the CPU faer provider supports it; the LAPACK provider
888/// and GPU backends are unsupported in this slice). AD through the full variant
889/// is unsupported and surfaces a typed error, not a silent thin fallback.
890pub fn svd_full(a: &EagerTensor) -> Result<(EagerTensor, EagerTensor, EagerTensor)> {
891    let mut outputs = apply_linalg_eager(LinalgOp::SvdFull, &[a])?.into_iter();
892    match (
893        outputs.next(),
894        outputs.next(),
895        outputs.next(),
896        outputs.next(),
897    ) {
898        (Some(u), Some(s), Some(vh), None) => Ok((u, s, vh)),
899        _ => Err(Error::Internal(
900            "svd_full eager op returned an unexpected number of outputs".to_string(),
901        )),
902    }
903}
904
905/// QR decomposition for eager tensors.
906///
907/// # Examples
908///
909/// ```rust
910/// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
911/// use tenferro_linalg::EagerTensorLinalgExt;
912///
913/// let ctx = EagerRuntime::new()?;
914/// let a = EagerTensor::from_tensor_in(
915///     Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 0.0, 0.0, 1.0]).unwrap(),
916///     ctx,
917/// ).unwrap();
918/// let (q, r) = a.qr()?;
919/// assert_eq!(q.shape(), &[2, 2]);
920/// assert_eq!(r.shape(), &[2, 2]);
921/// # Ok::<(), tenferro_ad::Error>(())
922/// ```
923///
924/// # Errors
925///
926/// Returns `Error::Validation` for an invalid rank or matrix shape,
927/// `Error::Extension` for an unsupported dtype or numerical failure, and
928/// `Error::RuntimeState` when the eager runtime or backend is unavailable.
929pub fn qr(a: &EagerTensor) -> Result<(EagerTensor, EagerTensor)> {
930    qr_with_options(a, QrOptions::default())
931}
932
933/// QR decomposition for eager tensors with explicit options.
934///
935/// `gauge` controls optional sign or phase post-processing.
936///
937/// # Examples
938///
939/// ```rust
940/// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
941/// use tenferro_linalg::{EagerTensorLinalgExt, QrGauge, QrOptions};
942///
943/// let ctx = EagerRuntime::new()?;
944/// let a = EagerTensor::from_tensor_in(
945///     Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 0.0, 0.0, 1.0]).unwrap(),
946///     ctx,
947/// ).unwrap();
948/// let (q, r) = a.qr_with_options(QrOptions::default().gauge(QrGauge::PositiveDiagonal))?;
949/// assert_eq!(q.shape(), &[2, 2]);
950/// assert_eq!(r.shape(), &[2, 2]);
951/// # Ok::<(), tenferro_ad::Error>(())
952/// ```
953///
954/// # Errors
955///
956/// Returns `Error::Validation` for an invalid rank or matrix shape,
957/// `Error::Extension` for an unsupported dtype or numerical failure, and
958/// `Error::Internal` if the extension returns an unexpected number of outputs.
959pub fn qr_with_options(a: &EagerTensor, options: QrOptions) -> Result<(EagerTensor, EagerTensor)> {
960    two_outputs(
961        apply_linalg_eager(
962            LinalgOp::Qr {
963                gauge: options.gauge,
964            },
965            &[a],
966        )?,
967        "qr",
968    )
969}
970
971/// LU factorization for eager tensors.
972///
973/// # Examples
974///
975/// ```rust
976/// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
977/// use tenferro_linalg::EagerTensorLinalgExt;
978///
979/// let ctx = EagerRuntime::new()?;
980/// let a = EagerTensor::from_tensor_in(
981///     Tensor::from_vec_col_major(vec![2, 2], vec![0.0_f64, 1.0, 1.0, 0.0]).unwrap(),
982///     ctx,
983/// ).unwrap();
984/// let (_p, l, u, parity) = a.lu()?;
985/// assert_eq!(l.shape(), &[2, 2]);
986/// assert_eq!(u.shape(), &[2, 2]);
987/// assert_eq!(parity.shape(), &[] as &[usize]);
988/// # Ok::<(), tenferro_ad::Error>(())
989/// ```
990///
991/// # Errors
992///
993/// Returns `Error::Validation` for an invalid rank or matrix shape,
994/// `Error::Extension` for an unsupported dtype or singular numerical result,
995/// and `Error::RuntimeState` when the eager runtime or backend is unavailable.
996pub fn lu(a: &EagerTensor) -> Result<(EagerTensor, EagerTensor, EagerTensor, EagerTensor)> {
997    let mut outputs = apply_linalg_eager(LinalgOp::Lu, &[a])?.into_iter();
998    match (
999        outputs.next(),
1000        outputs.next(),
1001        outputs.next(),
1002        outputs.next(),
1003        outputs.next(),
1004    ) {
1005        (Some(p), Some(l), Some(u), Some(parity), None) => Ok((p, l, u, parity)),
1006        _ => Err(Error::Internal(
1007            "lu eager op returned an unexpected number of outputs".to_string(),
1008        )),
1009    }
1010}
1011
1012/// Complete-pivot LU factorization for eager tensors.
1013///
1014/// Returns `(P, L, U, Q, parity)` with reconstruction convention
1015/// `A = P^T * L * U * Q`, equivalently `P * A * Q^T = L * U`. `parity` is a
1016/// scalar real tensor containing `+1` or `-1`: `F32` for `F32`/`C32` inputs and
1017/// `F64` for `F64`/`C64` inputs.
1018///
1019/// # Examples
1020///
1021/// ```rust
1022/// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
1023/// use tenferro_linalg::EagerTensorLinalgExt;
1024///
1025/// let ctx = EagerRuntime::new()?;
1026/// let a = EagerTensor::from_tensor_in(
1027///     Tensor::from_vec_col_major(vec![2, 2], vec![0.0_f64, 2.0, 1.0, 3.0]).unwrap(),
1028///     ctx,
1029/// ).unwrap();
1030/// let (p, _l, _u, q, parity) = a.full_piv_lu()?;
1031/// assert_eq!(p.shape(), &[2, 2]);
1032/// assert_eq!(q.shape(), &[2, 2]);
1033/// assert_eq!(parity.shape(), &[] as &[usize]);
1034/// # Ok::<(), tenferro_ad::Error>(())
1035/// ```
1036///
1037/// # Errors
1038///
1039/// Returns `Error::Validation` for an invalid rank or matrix shape,
1040/// `Error::Extension` for an unsupported dtype or singular numerical result,
1041/// and `Error::Internal` if the extension returns an unexpected number of
1042/// outputs.
1043pub fn full_piv_lu(
1044    a: &EagerTensor,
1045) -> Result<(
1046    EagerTensor,
1047    EagerTensor,
1048    EagerTensor,
1049    EagerTensor,
1050    EagerTensor,
1051)> {
1052    let mut outputs = apply_linalg_eager(LinalgOp::FullPivLu, &[a])?.into_iter();
1053    match (
1054        outputs.next(),
1055        outputs.next(),
1056        outputs.next(),
1057        outputs.next(),
1058        outputs.next(),
1059        outputs.next(),
1060    ) {
1061        (Some(p), Some(l), Some(u), Some(q), Some(parity), None) => Ok((p, l, u, q, parity)),
1062        _ => Err(Error::Internal(
1063            "full_piv_lu eager op returned an unexpected number of outputs".to_string(),
1064        )),
1065    }
1066}
1067
1068/// Solve a linear system using complete-pivot LU behavior.
1069///
1070/// # Examples
1071///
1072/// ```rust
1073/// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
1074/// use tenferro_linalg::EagerTensorLinalgExt;
1075///
1076/// let ctx = EagerRuntime::new()?;
1077/// let a = EagerTensor::from_tensor_in(
1078///     Tensor::from_vec_col_major(vec![2, 2], vec![0.0_f64, 2.0, 1.0, 3.0]).unwrap(),
1079///     ctx.clone(),
1080/// ).unwrap();
1081/// let b = EagerTensor::from_tensor_in(
1082///     Tensor::from_vec_col_major(vec![2, 1], vec![-1.0_f64, 5.0]).unwrap(),
1083///     ctx,
1084/// ).unwrap();
1085/// let x = a.full_piv_lu_solve(&b)?;
1086/// assert_eq!(x.shape(), &[2, 1]);
1087/// # Ok::<(), tenferro_ad::Error>(())
1088/// ```
1089///
1090/// # Errors
1091///
1092/// Returns `Error::Validation` when `a` and `b` have incompatible matrix or
1093/// batch shapes, `Error::Extension` for an unsupported dtype or singular
1094/// system, and `Error::RuntimeState` when the backend is unavailable.
1095pub fn full_piv_lu_solve(a: &EagerTensor, b: &EagerTensor) -> Result<EagerTensor> {
1096    one_output(
1097        apply_linalg_eager(LinalgOp::FullPivLuSolve { transpose_a: false }, &[a, b])?,
1098        "full_piv_lu_solve",
1099    )
1100}
1101
1102/// Solve a linear system for eager tensors.
1103///
1104/// # Examples
1105///
1106/// ```rust
1107/// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
1108/// use tenferro_linalg::EagerTensorLinalgExt;
1109///
1110/// let ctx = EagerRuntime::new()?;
1111/// let a = EagerTensor::from_tensor_in(
1112///     Tensor::from_vec_col_major(vec![2, 2], vec![2.0_f64, 0.0, 0.0, 4.0]).unwrap(),
1113///     ctx.clone(),
1114/// ).unwrap();
1115/// let b = EagerTensor::from_tensor_in(
1116///     Tensor::from_vec_col_major(vec![2, 1], vec![4.0_f64, 8.0]).unwrap(),
1117///     ctx,
1118/// ).unwrap();
1119/// let x = a.solve(&b)?;
1120/// assert_eq!(x.shape(), &[2, 1]);
1121/// # Ok::<(), tenferro_ad::Error>(())
1122/// ```
1123///
1124/// # Errors
1125///
1126/// Returns `Error::Validation` for incompatible matrix, batch, or dtype
1127/// metadata, `Error::Extension` for an unsupported dtype or singular system,
1128/// and `Error::RuntimeState` when the backend is unavailable.
1129pub fn solve(a: &EagerTensor, b: &EagerTensor) -> Result<EagerTensor> {
1130    one_output(apply_linalg_eager(LinalgOp::Solve, &[a, b])?, "solve")
1131}
1132
1133/// Cholesky factorization for eager tensors.
1134///
1135/// # Examples
1136///
1137/// ```rust
1138/// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
1139/// use tenferro_linalg::EagerTensorLinalgExt;
1140///
1141/// let ctx = EagerRuntime::new()?;
1142/// let a = EagerTensor::from_tensor_in(
1143///     Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 0.0, 0.0, 1.0]).unwrap(),
1144///     ctx,
1145/// ).unwrap();
1146/// let l = a.cholesky()?;
1147/// assert_eq!(l.shape(), &[2, 2]);
1148/// # Ok::<(), tenferro_ad::Error>(())
1149/// ```
1150///
1151/// # Errors
1152///
1153/// Returns `Error::Validation` for a non-square or invalid-rank input,
1154/// `Error::Extension` for an unsupported dtype or a non-positive-definite
1155/// matrix, and `Error::RuntimeState` when the backend is unavailable.
1156pub fn cholesky(a: &EagerTensor) -> Result<EagerTensor> {
1157    one_output(apply_linalg_eager(LinalgOp::Cholesky, &[a])?, "cholesky")
1158}
1159
1160/// Hermitian eigenvalue decomposition for eager tensors.
1161///
1162/// # Examples
1163///
1164/// ```rust
1165/// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
1166/// use tenferro_linalg::EagerTensorLinalgExt;
1167///
1168/// let ctx = EagerRuntime::new()?;
1169/// let a = EagerTensor::from_tensor_in(
1170///     Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 0.0, 0.0, 3.0]).unwrap(),
1171///     ctx,
1172/// ).unwrap();
1173/// let (values, vectors) = a.eigh()?;
1174/// assert_eq!(values.shape(), &[2]);
1175/// assert_eq!(vectors.shape(), &[2, 2]);
1176/// # Ok::<(), tenferro_ad::Error>(())
1177/// ```
1178///
1179/// # Errors
1180///
1181/// Returns `Error::Validation` for a non-square or invalid-rank input,
1182/// `Error::Extension` for an unsupported dtype or numerical non-convergence,
1183/// and `Error::RuntimeState` when the backend is unavailable.
1184pub fn eigh(a: &EagerTensor) -> Result<(EagerTensor, EagerTensor)> {
1185    eigh_with_options(a, EighOptions::default())
1186}
1187
1188/// Hermitian eigenvalue decomposition for eager tensors with explicit options.
1189///
1190/// `derivative_eps` regularizes derivative formulas for repeated or nearly
1191/// repeated eigenvalues. It is not a backend eigensolver tolerance.
1192///
1193/// # Examples
1194///
1195/// ```rust
1196/// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
1197/// use tenferro_linalg::{EagerTensorLinalgExt, EighGauge, EighOptions};
1198///
1199/// let ctx = EagerRuntime::new()?;
1200/// let a = EagerTensor::from_tensor_in(
1201///     Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 0.0, 0.0, 3.0]).unwrap(),
1202///     ctx,
1203/// ).unwrap();
1204/// let (values, vectors) = a
1205///     .eigh_with_options(
1206///         EighOptions::default()
1207///             .gauge(EighGauge::CanonicalPivot)
1208///             .derivative_eps(1.0e-10),
1209///     )?;
1210/// assert_eq!(values.shape(), &[2]);
1211/// assert_eq!(vectors.shape(), &[2, 2]);
1212/// # Ok::<(), tenferro_ad::Error>(())
1213/// ```
1214///
1215/// # Errors
1216///
1217/// Returns `Error::Validation` for an invalid rank, shape, or
1218/// `derivative_eps`, `Error::Extension` for unsupported dtypes or numerical
1219/// non-convergence, and `Error::Internal` for an output-count violation.
1220pub fn eigh_with_options(
1221    a: &EagerTensor,
1222    options: EighOptions,
1223) -> Result<(EagerTensor, EagerTensor)> {
1224    validate_derivative_eps("eigh_with_options", options.derivative_eps)?;
1225    two_outputs(
1226        apply_linalg_eager(
1227            LinalgOp::Eigh {
1228                derivative_eps: options.derivative_eps,
1229                gauge: options.gauge,
1230            },
1231            &[a],
1232        )?,
1233        "eigh",
1234    )
1235}
1236
1237/// General eigenvalue decomposition for eager tensors.
1238///
1239/// # Examples
1240///
1241/// ```rust
1242/// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
1243/// use tenferro_linalg::EagerTensorLinalgExt;
1244///
1245/// let ctx = EagerRuntime::new()?;
1246/// let a = EagerTensor::from_tensor_in(
1247///     Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 0.0, 0.0, 3.0]).unwrap(),
1248///     ctx,
1249/// ).unwrap();
1250/// let (values, vectors) = a.eig()?;
1251/// assert_eq!(values.shape(), &[2]);
1252/// assert_eq!(vectors.shape(), &[2, 2]);
1253/// # Ok::<(), tenferro_ad::Error>(())
1254/// ```
1255///
1256/// # Errors
1257///
1258/// Returns `Error::Validation` for a non-square or invalid-rank input,
1259/// `Error::Extension` for an unsupported dtype or numerical non-convergence,
1260/// and `Error::RuntimeState` when the backend is unavailable.
1261pub fn eig(a: &EagerTensor) -> Result<(EagerTensor, EagerTensor)> {
1262    two_outputs(
1263        apply_linalg_eager(
1264            LinalgOp::Eig {
1265                input_dtype: a.dtype(),
1266            },
1267            &[a],
1268        )?,
1269        "eig",
1270    )
1271}
1272
1273/// Triangular solve for eager tensors.
1274///
1275/// # Examples
1276///
1277/// ```rust
1278/// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
1279/// use tenferro_linalg::EagerTensorLinalgExt;
1280///
1281/// let ctx = EagerRuntime::new()?;
1282/// let a = EagerTensor::from_tensor_in(
1283///     Tensor::from_vec_col_major(vec![2, 2], vec![2.0_f64, 0.0, 1.0, 3.0]).unwrap(),
1284///     ctx.clone(),
1285/// ).unwrap();
1286/// let b = EagerTensor::from_tensor_in(
1287///     Tensor::from_vec_col_major(vec![2, 1], vec![2.0_f64, 7.0]).unwrap(),
1288///     ctx,
1289/// ).unwrap();
1290/// let x = a.triangular_solve(&b, true, true, false, false)?;
1291/// assert_eq!(x.shape(), &[2, 1]);
1292/// # Ok::<(), tenferro_ad::Error>(())
1293/// ```
1294///
1295/// # Errors
1296///
1297/// Returns `Error::Validation` for incompatible matrix, batch, or dtype
1298/// metadata, `Error::Extension` for an unsupported dtype or singular system,
1299/// and `Error::RuntimeState` when the backend is unavailable.
1300pub fn triangular_solve(
1301    a: &EagerTensor,
1302    b: &EagerTensor,
1303    left_side: bool,
1304    lower: bool,
1305    transpose_a: bool,
1306    unit_diagonal: bool,
1307) -> Result<EagerTensor> {
1308    one_output(
1309        apply_linalg_eager(
1310            LinalgOp::TriangularSolve {
1311                left_side,
1312                lower,
1313                transpose_a,
1314                unit_diagonal,
1315            },
1316            &[a, b],
1317        )?,
1318        "triangular_solve",
1319    )
1320}
1321
1322pub(crate) fn one_output(outputs: Vec<EagerTensor>, name: &str) -> Result<EagerTensor> {
1323    let mut outputs = outputs.into_iter();
1324    match (outputs.next(), outputs.next()) {
1325        (Some(output), None) => Ok(output),
1326        _ => Err(Error::Internal(format!(
1327            "{name} eager op returned an unexpected number of outputs"
1328        ))),
1329    }
1330}
1331
1332fn two_outputs(outputs: Vec<EagerTensor>, name: &str) -> Result<(EagerTensor, EagerTensor)> {
1333    let mut outputs = outputs.into_iter();
1334    match (outputs.next(), outputs.next(), outputs.next()) {
1335        (Some(lhs), Some(rhs), None) => Ok((lhs, rhs)),
1336        _ => Err(Error::Internal(format!(
1337            "{name} eager op returned an unexpected number of outputs"
1338        ))),
1339    }
1340}