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 execute_linalg_extension_reads, extension_module, validate_derivative_eps, EighOptions,
11 LinalgExtensionOp, LinalgOp, QrOptions, 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 fn svd(&self) -> Result<(EagerTensor, EagerTensor, EagerTensor)>;
23 /// # Errors
24 ///
25 /// Returns `Error::Validation` when `derivative_eps` is non-finite or
26 /// non-positive, `Error::Extension` for unsupported dtypes or numerical
27 /// non-convergence, and `Error::Internal` if the extension violates its
28 /// output-count contract.
29 fn svd_with_options(
30 &self,
31 options: SvdOptions,
32 ) -> Result<(EagerTensor, EagerTensor, EagerTensor)>;
33 /// Full-matrices SVD returning square `U (m x m)` and `Vh (n x n)`, whose
34 /// trailing `n - rank` rows span the input's right nullspace.
35 ///
36 /// # Errors
37 ///
38 /// Returns `Error::Validation` for an invalid rank, and `Error::Extension`
39 /// with an unsupported-operation source when the active backend does not
40 /// implement full-matrices SVD (the CPU faer provider supports it; the
41 /// LAPACK provider and GPU backends return an unsupported error in this
42 /// slice). Automatic differentiation through the full variant is
43 /// unsupported and surfaces a typed error rather than a silent thin
44 /// fallback.
45 fn svd_full(&self) -> Result<(EagerTensor, EagerTensor, EagerTensor)>;
46 /// # Errors
47 ///
48 /// Returns `Error::Validation` for invalid matrix rank or shape,
49 /// `Error::Extension` for unsupported dtypes or numerical failure, and
50 /// `Error::RuntimeState` when the eager runtime or backend is unavailable.
51 fn qr(&self) -> Result<(EagerTensor, EagerTensor)>;
52 /// # Errors
53 ///
54 /// Returns `Error::Validation` for invalid matrix rank or shape,
55 /// `Error::Extension` for unsupported dtypes or numerical failure, and
56 /// `Error::Internal` if the extension violates its output-count contract.
57 fn qr_with_options(&self, options: QrOptions) -> Result<(EagerTensor, EagerTensor)>;
58 /// # Errors
59 ///
60 /// Returns `Error::Validation` for an invalid matrix rank or shape,
61 /// `Error::Extension` for an unsupported dtype or singular numerical
62 /// result, and `Error::RuntimeState` when execution cannot access its
63 /// backend.
64 fn lu(&self) -> Result<(EagerTensor, EagerTensor, EagerTensor, EagerTensor)>;
65 /// # Errors
66 ///
67 /// Returns `Error::Validation` for an invalid matrix rank or shape,
68 /// `Error::Extension` for unsupported dtypes or singular numerical
69 /// results, and `Error::Internal` if the extension violates its output
70 /// contract.
71 fn full_piv_lu(
72 &self,
73 ) -> Result<(
74 EagerTensor,
75 EagerTensor,
76 EagerTensor,
77 EagerTensor,
78 EagerTensor,
79 )>;
80 /// # Errors
81 ///
82 /// Returns `Error::Validation` when `a` and `b` have incompatible matrix
83 /// or batch shapes, `Error::Extension` for an unsupported dtype or
84 /// singular system, and `Error::RuntimeState` when the backend is
85 /// unavailable.
86 fn full_piv_lu_solve(&self, b: &EagerTensor) -> Result<EagerTensor>;
87 /// # Errors
88 ///
89 /// Returns `Error::Validation` for incompatible matrix, batch, or dtype
90 /// metadata, `Error::Extension` for an unsupported dtype or singular
91 /// system, and `Error::RuntimeState` when the backend is unavailable.
92 fn solve(&self, b: &EagerTensor) -> Result<EagerTensor>;
93 /// Least-squares solve `argmin_x ||A x - b||_2` for a tall or square,
94 /// full-column-rank `A`, via the thin QR factorization.
95 ///
96 /// # Errors
97 ///
98 /// Returns `Error::Validation` when `A` or `b` is not a matrix (rank
99 /// `>= 2`), when `A` is wide (`rows < cols`; underdetermined), or when the
100 /// dtype is not floating-point or complex; `Error::Extension` for backend
101 /// QR or triangular-solve failures; and `Error::RuntimeState` when the
102 /// backend is unavailable. Rank-deficient `A` is not detected and yields an
103 /// ill-defined result.
104 fn lstsq(&self, b: &EagerTensor) -> Result<EagerTensor>;
105 /// # Errors
106 ///
107 /// Returns `Error::Validation` for a non-square or invalid-rank input,
108 /// `Error::Extension` for unsupported dtypes or a non-positive-definite
109 /// matrix, and `Error::RuntimeState` when the backend is unavailable.
110 fn cholesky(&self) -> Result<EagerTensor>;
111 /// # Errors
112 ///
113 /// Returns `Error::Validation` for a non-square or invalid-rank input,
114 /// `Error::Extension` for unsupported dtypes or numerical non-convergence,
115 /// and `Error::RuntimeState` when the backend is unavailable.
116 fn eigh(&self) -> Result<(EagerTensor, EagerTensor)>;
117 /// # Errors
118 ///
119 /// Returns `Error::Validation` for an invalid rank, shape, or
120 /// `derivative_eps`, `Error::Extension` for unsupported dtypes or
121 /// non-convergence, and `Error::Internal` for an output-count violation.
122 fn eigh_with_options(&self, options: EighOptions) -> Result<(EagerTensor, EagerTensor)>;
123 /// # Errors
124 ///
125 /// Returns `Error::Validation` for a non-square or invalid-rank input,
126 /// `Error::Extension` for unsupported dtypes or numerical non-convergence,
127 /// and `Error::RuntimeState` when the backend is unavailable.
128 fn eig(&self) -> Result<(EagerTensor, EagerTensor)>;
129 /// # Errors
130 ///
131 /// Returns `Error::Validation` for incompatible matrix, batch, or dtype
132 /// metadata, `Error::Extension` for unsupported dtypes or a singular
133 /// system, and `Error::RuntimeState` when the backend is unavailable.
134 fn triangular_solve(
135 &self,
136 b: &EagerTensor,
137 left_side: bool,
138 lower: bool,
139 transpose_a: bool,
140 unit_diagonal: bool,
141 ) -> Result<EagerTensor>;
142 /// Return the determinant sign and logarithm of its absolute value.
143 ///
144 /// # Errors
145 ///
146 /// Returns `Error::Validation` for a non-square input, `Error::Extension`
147 /// for an unsupported dtype or numerical failure, and `Error::Internal`
148 /// if a primitive violates its output contract.
149 fn slogdet(&self) -> Result<(EagerTensor, EagerTensor)>;
150 /// Return the determinant.
151 ///
152 /// # Errors
153 ///
154 /// Returns `Error::Validation`, `Error::Extension`, `Error::RuntimeState`,
155 /// or `Error::Internal` under the conditions documented by [`Self::slogdet`].
156 fn det(&self) -> Result<EagerTensor>;
157 /// Return the matrix inverse.
158 ///
159 /// # Errors
160 ///
161 /// Returns `Error::Validation` for invalid matrix metadata,
162 /// `Error::Extension` for an unsupported dtype or singular solve, and
163 /// `Error::RuntimeState` when eager execution cannot access its backend.
164 fn inv(&self) -> Result<EagerTensor>;
165 /// Return Hermitian eigenvalues without eigenvectors.
166 ///
167 /// # Errors
168 ///
169 /// Returns the validation, unsupported-dtype, numerical-convergence,
170 /// runtime-state, or output-contract errors reported by [`Self::eigh`].
171 fn eigvalsh(&self) -> Result<EagerTensor>;
172 /// Return general eigenvalues without eigenvectors.
173 ///
174 /// # Errors
175 ///
176 /// Returns the validation, unsupported-dtype, numerical-convergence,
177 /// runtime-state, or output-contract errors reported by [`Self::eig`].
178 fn eigvals(&self) -> Result<EagerTensor>;
179 /// Return the Moore-Penrose pseudoinverse with the default tolerance.
180 ///
181 /// # Errors
182 ///
183 /// Returns `Error::Validation` for invalid matrix metadata,
184 /// `Error::Extension` for unsupported SVD execution, and
185 /// `Error::Internal` for an unexpected decomposition output contract.
186 fn pinv(&self) -> Result<EagerTensor>;
187 /// Return the Moore-Penrose pseudoinverse with an explicit relative tolerance.
188 ///
189 /// # Errors
190 ///
191 /// Returns `Error::Validation` when `rtol` is negative or non-finite, plus
192 /// the `Error::Extension` and output-contract errors reported by [`Self::pinv`].
193 fn pinv_with_rtol(&self, rtol: f64) -> Result<EagerTensor>;
194 /// Return a vector, matrix, or tensor norm.
195 ///
196 /// # Errors
197 ///
198 /// Returns `Error::Validation` for an unsupported order/dimension
199 /// combination or invalid axes, `Error::Extension` when a required matrix
200 /// decomposition is unsupported, and eager runtime/backend failures.
201 fn norm(&self, ord: Option<f64>, dim: Option<&[usize]>, keepdim: bool) -> Result<EagerTensor>;
202}
203
204impl EagerTensorLinalgExt for EagerTensor {
205 fn svd(&self) -> Result<(EagerTensor, EagerTensor, EagerTensor)> {
206 svd(self)
207 }
208
209 fn svd_with_options(
210 &self,
211 options: SvdOptions,
212 ) -> Result<(EagerTensor, EagerTensor, EagerTensor)> {
213 svd_with_options(self, options)
214 }
215
216 fn svd_full(&self) -> Result<(EagerTensor, EagerTensor, EagerTensor)> {
217 svd_full(self)
218 }
219
220 fn qr(&self) -> Result<(EagerTensor, EagerTensor)> {
221 qr(self)
222 }
223
224 fn qr_with_options(&self, options: QrOptions) -> Result<(EagerTensor, EagerTensor)> {
225 qr_with_options(self, options)
226 }
227
228 fn lu(&self) -> Result<(EagerTensor, EagerTensor, EagerTensor, EagerTensor)> {
229 lu(self)
230 }
231
232 fn full_piv_lu(
233 &self,
234 ) -> Result<(
235 EagerTensor,
236 EagerTensor,
237 EagerTensor,
238 EagerTensor,
239 EagerTensor,
240 )> {
241 full_piv_lu(self)
242 }
243
244 fn full_piv_lu_solve(&self, b: &EagerTensor) -> Result<EagerTensor> {
245 full_piv_lu_solve(self, b)
246 }
247
248 fn solve(&self, b: &EagerTensor) -> Result<EagerTensor> {
249 solve(self, b)
250 }
251
252 fn lstsq(&self, b: &EagerTensor) -> Result<EagerTensor> {
253 eager_composites::lstsq(self, b)
254 }
255
256 fn cholesky(&self) -> Result<EagerTensor> {
257 cholesky(self)
258 }
259
260 fn eigh(&self) -> Result<(EagerTensor, EagerTensor)> {
261 eigh(self)
262 }
263
264 fn eigh_with_options(&self, options: EighOptions) -> Result<(EagerTensor, EagerTensor)> {
265 eigh_with_options(self, options)
266 }
267
268 fn eig(&self) -> Result<(EagerTensor, EagerTensor)> {
269 eig(self)
270 }
271
272 fn triangular_solve(
273 &self,
274 b: &EagerTensor,
275 left_side: bool,
276 lower: bool,
277 transpose_a: bool,
278 unit_diagonal: bool,
279 ) -> Result<EagerTensor> {
280 triangular_solve(self, b, left_side, lower, transpose_a, unit_diagonal)
281 }
282
283 fn slogdet(&self) -> Result<(EagerTensor, EagerTensor)> {
284 eager_composites::slogdet(self)
285 }
286
287 fn det(&self) -> Result<EagerTensor> {
288 eager_composites::det(self)
289 }
290
291 fn inv(&self) -> Result<EagerTensor> {
292 eager_composites::inv(self)
293 }
294
295 fn eigvalsh(&self) -> Result<EagerTensor> {
296 eager_composites::eigvalsh(self)
297 }
298
299 fn eigvals(&self) -> Result<EagerTensor> {
300 eager_composites::eigvals(self)
301 }
302
303 fn pinv(&self) -> Result<EagerTensor> {
304 eager_composites::pinv(self)
305 }
306
307 fn pinv_with_rtol(&self, rtol: f64) -> Result<EagerTensor> {
308 eager_composites::pinv_with_rtol(self, rtol)
309 }
310
311 fn norm(&self, ord: Option<f64>, dim: Option<&[usize]>, keepdim: bool) -> Result<EagerTensor> {
312 eager_composites::norm(self, ord, dim, keepdim)
313 }
314}
315
316fn apply_linalg_eager(op: LinalgOp, inputs: &[&EagerTensor]) -> Result<Vec<EagerTensor>> {
317 let op = Arc::new(LinalgExtensionOp::new(op));
318 let execute_op = Arc::clone(&op);
319 let module = eager_cpu_extension_module()?;
320 apply_eager_with_extension_session(op, inputs, module, move |_op, input_reads, ctx| {
321 execute_linalg_extension_reads(&execute_op, input_reads, ctx)
322 })
323}
324
325fn eager_cpu_extension_module() -> Result<Arc<dyn ExtensionModule>> {
326 static MODULE: OnceLock<Arc<dyn ExtensionModule>> = OnceLock::new();
327 if let Some(module) = MODULE.get() {
328 return Ok(Arc::clone(module));
329 }
330
331 let engine_id = tenferro_cpu::runtime_engine_id().map_err(eager_runtime_config_error)?;
332 let module = extension_module::<tenferro_cpu::CpuBackend>(engine_id)
333 .map_err(eager_runtime_config_error)?;
334 let _ = MODULE.set(Arc::clone(&module));
335 Ok(MODULE.get().cloned().unwrap_or(module))
336}
337
338fn eager_runtime_config_error(source: tenferro_runtime::RuntimeConfigError) -> Error {
339 Error::runtime_state_source(
340 "tenferro_linalg::eager_extension_module",
341 ErrorPhase::Execution,
342 source,
343 )
344}
345
346/// Singular value decomposition for eager tensors.
347///
348/// # Examples
349///
350/// ```rust
351/// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
352/// use tenferro_linalg::EagerTensorLinalgExt;
353///
354/// let ctx = EagerRuntime::new()?;
355/// let a = EagerTensor::from_tensor_in(
356/// Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 0.0, 0.0, 2.0]).unwrap(),
357/// ctx,
358/// ).unwrap();
359/// let (_u, s, _vt) = a.svd()?;
360/// assert_eq!(s.shape(), &[2]);
361/// # Ok::<(), tenferro_ad::Error>(())
362/// ```
363///
364/// # Errors
365///
366/// Returns `Error::Validation` for an invalid rank, matrix shape, or dtype,
367/// `Error::Extension` with an unsupported-dtype or non-convergence source when
368/// the backend cannot compute the decomposition, and `Error::RuntimeState`
369/// when the eager runtime or backend is unavailable.
370pub fn svd(a: &EagerTensor) -> Result<(EagerTensor, EagerTensor, EagerTensor)> {
371 svd_with_options(a, SvdOptions::default())
372}
373
374/// Singular value decomposition for eager tensors with explicit options.
375///
376/// `derivative_eps` regularizes decomposition derivative formulas. It is not a
377/// backend SVD solver tolerance.
378///
379/// # Examples
380///
381/// ```rust
382/// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
383/// use tenferro_linalg::{EagerTensorLinalgExt, SvdGauge, SvdOptions};
384///
385/// let ctx = EagerRuntime::new()?;
386/// let a = EagerTensor::from_tensor_in(
387/// Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 0.0, 0.0, 2.0]).unwrap(),
388/// ctx,
389/// ).unwrap();
390/// let options = SvdOptions::default()
391/// .gauge(SvdGauge::CanonicalPivot)
392/// .derivative_eps(1.0e-10);
393/// let (_u, s, _vt) = a.svd_with_options(options)?;
394/// assert_eq!(s.shape(), &[2]);
395/// # Ok::<(), tenferro_ad::Error>(())
396/// ```
397///
398/// # Errors
399///
400/// Returns `Error::Validation` when `derivative_eps` is non-finite or
401/// non-positive, `Error::Extension` for unsupported dtypes or numerical
402/// non-convergence, and `Error::Internal` if the extension returns an
403/// unexpected number of outputs.
404pub fn svd_with_options(
405 a: &EagerTensor,
406 options: SvdOptions,
407) -> Result<(EagerTensor, EagerTensor, EagerTensor)> {
408 validate_derivative_eps("svd_with_options", options.derivative_eps)?;
409 let mut outputs = apply_linalg_eager(
410 LinalgOp::Svd {
411 derivative_eps: options.derivative_eps,
412 gauge: options.gauge,
413 },
414 &[a],
415 )?
416 .into_iter();
417 match (
418 outputs.next(),
419 outputs.next(),
420 outputs.next(),
421 outputs.next(),
422 ) {
423 (Some(u), Some(s), Some(vt), None) => Ok((u, s, vt)),
424 _ => Err(Error::Internal(
425 "svd eager op returned an unexpected number of outputs".to_string(),
426 )),
427 }
428}
429
430/// Full-matrices singular value decomposition for eager tensors.
431///
432/// Returns `(U, S, Vh)` with square `U` (`m x m`) and `Vh` (`n x n`); `S` holds
433/// `min(m, n)` singular values. The trailing `n - rank` rows of `Vh` span the
434/// input's right nullspace, which the thin [`svd`] cannot represent.
435///
436/// # Examples
437///
438/// ```rust
439/// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
440/// use tenferro_linalg::EagerTensorLinalgExt;
441///
442/// let ctx = EagerRuntime::new()?;
443/// let a = EagerTensor::from_tensor_in(
444/// Tensor::from_vec_col_major(vec![1, 2], vec![1.0_f64, 1.0]).unwrap(),
445/// ctx,
446/// ).unwrap();
447/// // Full SVD is provided by the CPU faer backend; other providers (e.g. the
448/// // LAPACK provider) report it as unsupported, so guard on the result.
449/// if let Ok((u, s, vh)) = a.svd_full() {
450/// assert_eq!(u.shape(), &[1, 1]);
451/// assert_eq!(s.shape(), &[1]);
452/// assert_eq!(vh.shape(), &[2, 2]);
453/// }
454/// # Ok::<(), tenferro_ad::Error>(())
455/// ```
456///
457/// # Errors
458///
459/// Returns `Error::Validation` for an invalid rank and `Error::Extension` with
460/// an unsupported-operation source when the active backend does not implement
461/// full-matrices SVD (the CPU faer provider supports it; the LAPACK provider
462/// and GPU backends are unsupported in this slice). AD through the full variant
463/// is unsupported and surfaces a typed error, not a silent thin fallback.
464pub fn svd_full(a: &EagerTensor) -> Result<(EagerTensor, EagerTensor, EagerTensor)> {
465 let mut outputs = apply_linalg_eager(LinalgOp::SvdFull, &[a])?.into_iter();
466 match (
467 outputs.next(),
468 outputs.next(),
469 outputs.next(),
470 outputs.next(),
471 ) {
472 (Some(u), Some(s), Some(vh), None) => Ok((u, s, vh)),
473 _ => Err(Error::Internal(
474 "svd_full eager op returned an unexpected number of outputs".to_string(),
475 )),
476 }
477}
478
479/// QR decomposition for eager tensors.
480///
481/// # Examples
482///
483/// ```rust
484/// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
485/// use tenferro_linalg::EagerTensorLinalgExt;
486///
487/// let ctx = EagerRuntime::new()?;
488/// let a = EagerTensor::from_tensor_in(
489/// Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 0.0, 0.0, 1.0]).unwrap(),
490/// ctx,
491/// ).unwrap();
492/// let (q, r) = a.qr()?;
493/// assert_eq!(q.shape(), &[2, 2]);
494/// assert_eq!(r.shape(), &[2, 2]);
495/// # Ok::<(), tenferro_ad::Error>(())
496/// ```
497///
498/// # Errors
499///
500/// Returns `Error::Validation` for an invalid rank or matrix shape,
501/// `Error::Extension` for an unsupported dtype or numerical failure, and
502/// `Error::RuntimeState` when the eager runtime or backend is unavailable.
503pub fn qr(a: &EagerTensor) -> Result<(EagerTensor, EagerTensor)> {
504 qr_with_options(a, QrOptions::default())
505}
506
507/// QR decomposition for eager tensors with explicit options.
508///
509/// `gauge` controls optional sign or phase post-processing.
510///
511/// # Examples
512///
513/// ```rust
514/// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
515/// use tenferro_linalg::{EagerTensorLinalgExt, QrGauge, QrOptions};
516///
517/// let ctx = EagerRuntime::new()?;
518/// let a = EagerTensor::from_tensor_in(
519/// Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 0.0, 0.0, 1.0]).unwrap(),
520/// ctx,
521/// ).unwrap();
522/// let (q, r) = a.qr_with_options(QrOptions::default().gauge(QrGauge::PositiveDiagonal))?;
523/// assert_eq!(q.shape(), &[2, 2]);
524/// assert_eq!(r.shape(), &[2, 2]);
525/// # Ok::<(), tenferro_ad::Error>(())
526/// ```
527///
528/// # Errors
529///
530/// Returns `Error::Validation` for an invalid rank or matrix shape,
531/// `Error::Extension` for an unsupported dtype or numerical failure, and
532/// `Error::Internal` if the extension returns an unexpected number of outputs.
533pub fn qr_with_options(a: &EagerTensor, options: QrOptions) -> Result<(EagerTensor, EagerTensor)> {
534 two_outputs(
535 apply_linalg_eager(
536 LinalgOp::Qr {
537 gauge: options.gauge,
538 },
539 &[a],
540 )?,
541 "qr",
542 )
543}
544
545/// LU factorization for eager tensors.
546///
547/// # Examples
548///
549/// ```rust
550/// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
551/// use tenferro_linalg::EagerTensorLinalgExt;
552///
553/// let ctx = EagerRuntime::new()?;
554/// let a = EagerTensor::from_tensor_in(
555/// Tensor::from_vec_col_major(vec![2, 2], vec![0.0_f64, 1.0, 1.0, 0.0]).unwrap(),
556/// ctx,
557/// ).unwrap();
558/// let (_p, l, u, parity) = a.lu()?;
559/// assert_eq!(l.shape(), &[2, 2]);
560/// assert_eq!(u.shape(), &[2, 2]);
561/// assert_eq!(parity.shape(), &[] as &[usize]);
562/// # Ok::<(), tenferro_ad::Error>(())
563/// ```
564///
565/// # Errors
566///
567/// Returns `Error::Validation` for an invalid rank or matrix shape,
568/// `Error::Extension` for an unsupported dtype or singular numerical result,
569/// and `Error::RuntimeState` when the eager runtime or backend is unavailable.
570pub fn lu(a: &EagerTensor) -> Result<(EagerTensor, EagerTensor, EagerTensor, EagerTensor)> {
571 let mut outputs = apply_linalg_eager(LinalgOp::Lu, &[a])?.into_iter();
572 match (
573 outputs.next(),
574 outputs.next(),
575 outputs.next(),
576 outputs.next(),
577 outputs.next(),
578 ) {
579 (Some(p), Some(l), Some(u), Some(parity), None) => Ok((p, l, u, parity)),
580 _ => Err(Error::Internal(
581 "lu eager op returned an unexpected number of outputs".to_string(),
582 )),
583 }
584}
585
586/// Complete-pivot LU factorization for eager tensors.
587///
588/// Returns `(P, L, U, Q, parity)` with reconstruction convention
589/// `A = P^T * L * U * Q`, equivalently `P * A * Q^T = L * U`. `parity` is a
590/// scalar real tensor containing `+1` or `-1`: `F32` for `F32`/`C32` inputs and
591/// `F64` for `F64`/`C64` inputs.
592///
593/// # Examples
594///
595/// ```rust
596/// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
597/// use tenferro_linalg::EagerTensorLinalgExt;
598///
599/// let ctx = EagerRuntime::new()?;
600/// let a = EagerTensor::from_tensor_in(
601/// Tensor::from_vec_col_major(vec![2, 2], vec![0.0_f64, 2.0, 1.0, 3.0]).unwrap(),
602/// ctx,
603/// ).unwrap();
604/// let (p, _l, _u, q, parity) = a.full_piv_lu()?;
605/// assert_eq!(p.shape(), &[2, 2]);
606/// assert_eq!(q.shape(), &[2, 2]);
607/// assert_eq!(parity.shape(), &[] as &[usize]);
608/// # Ok::<(), tenferro_ad::Error>(())
609/// ```
610///
611/// # Errors
612///
613/// Returns `Error::Validation` for an invalid rank or matrix shape,
614/// `Error::Extension` for an unsupported dtype or singular numerical result,
615/// and `Error::Internal` if the extension returns an unexpected number of
616/// outputs.
617pub fn full_piv_lu(
618 a: &EagerTensor,
619) -> Result<(
620 EagerTensor,
621 EagerTensor,
622 EagerTensor,
623 EagerTensor,
624 EagerTensor,
625)> {
626 let mut outputs = apply_linalg_eager(LinalgOp::FullPivLu, &[a])?.into_iter();
627 match (
628 outputs.next(),
629 outputs.next(),
630 outputs.next(),
631 outputs.next(),
632 outputs.next(),
633 outputs.next(),
634 ) {
635 (Some(p), Some(l), Some(u), Some(q), Some(parity), None) => Ok((p, l, u, q, parity)),
636 _ => Err(Error::Internal(
637 "full_piv_lu eager op returned an unexpected number of outputs".to_string(),
638 )),
639 }
640}
641
642/// Solve a linear system using complete-pivot LU behavior.
643///
644/// # Examples
645///
646/// ```rust
647/// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
648/// use tenferro_linalg::EagerTensorLinalgExt;
649///
650/// let ctx = EagerRuntime::new()?;
651/// let a = EagerTensor::from_tensor_in(
652/// Tensor::from_vec_col_major(vec![2, 2], vec![0.0_f64, 2.0, 1.0, 3.0]).unwrap(),
653/// ctx.clone(),
654/// ).unwrap();
655/// let b = EagerTensor::from_tensor_in(
656/// Tensor::from_vec_col_major(vec![2, 1], vec![-1.0_f64, 5.0]).unwrap(),
657/// ctx,
658/// ).unwrap();
659/// let x = a.full_piv_lu_solve(&b)?;
660/// assert_eq!(x.shape(), &[2, 1]);
661/// # Ok::<(), tenferro_ad::Error>(())
662/// ```
663///
664/// # Errors
665///
666/// Returns `Error::Validation` when `a` and `b` have incompatible matrix or
667/// batch shapes, `Error::Extension` for an unsupported dtype or singular
668/// system, and `Error::RuntimeState` when the backend is unavailable.
669pub fn full_piv_lu_solve(a: &EagerTensor, b: &EagerTensor) -> Result<EagerTensor> {
670 one_output(
671 apply_linalg_eager(LinalgOp::FullPivLuSolve { transpose_a: false }, &[a, b])?,
672 "full_piv_lu_solve",
673 )
674}
675
676/// Solve a linear system for eager tensors.
677///
678/// # Examples
679///
680/// ```rust
681/// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
682/// use tenferro_linalg::EagerTensorLinalgExt;
683///
684/// let ctx = EagerRuntime::new()?;
685/// let a = EagerTensor::from_tensor_in(
686/// Tensor::from_vec_col_major(vec![2, 2], vec![2.0_f64, 0.0, 0.0, 4.0]).unwrap(),
687/// ctx.clone(),
688/// ).unwrap();
689/// let b = EagerTensor::from_tensor_in(
690/// Tensor::from_vec_col_major(vec![2, 1], vec![4.0_f64, 8.0]).unwrap(),
691/// ctx,
692/// ).unwrap();
693/// let x = a.solve(&b)?;
694/// assert_eq!(x.shape(), &[2, 1]);
695/// # Ok::<(), tenferro_ad::Error>(())
696/// ```
697///
698/// # Errors
699///
700/// Returns `Error::Validation` for incompatible matrix, batch, or dtype
701/// metadata, `Error::Extension` for an unsupported dtype or singular system,
702/// and `Error::RuntimeState` when the backend is unavailable.
703pub fn solve(a: &EagerTensor, b: &EagerTensor) -> Result<EagerTensor> {
704 let mut factor_outputs = apply_linalg_eager(LinalgOp::LuFactor, &[a])?.into_iter();
705 let (packed_lu, pivots) = match (
706 factor_outputs.next(),
707 factor_outputs.next(),
708 factor_outputs.next(),
709 factor_outputs.next(),
710 ) {
711 (Some(packed_lu), Some(pivots), Some(_parity), None) => (packed_lu, pivots),
712 _ => {
713 return Err(Error::Internal(
714 "lu_factor eager op returned an unexpected number of outputs".to_string(),
715 ));
716 }
717 };
718 one_output(
719 apply_linalg_eager(
720 LinalgOp::LuSolvePrepared {
721 transpose_a: false,
722 conjugate_a: false,
723 },
724 &[a, &packed_lu, &pivots, b],
725 )?,
726 "solve",
727 )
728}
729
730/// Cholesky factorization for eager tensors.
731///
732/// # Examples
733///
734/// ```rust
735/// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
736/// use tenferro_linalg::EagerTensorLinalgExt;
737///
738/// let ctx = EagerRuntime::new()?;
739/// let a = EagerTensor::from_tensor_in(
740/// Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 0.0, 0.0, 1.0]).unwrap(),
741/// ctx,
742/// ).unwrap();
743/// let l = a.cholesky()?;
744/// assert_eq!(l.shape(), &[2, 2]);
745/// # Ok::<(), tenferro_ad::Error>(())
746/// ```
747///
748/// # Errors
749///
750/// Returns `Error::Validation` for a non-square or invalid-rank input,
751/// `Error::Extension` for an unsupported dtype or a non-positive-definite
752/// matrix, and `Error::RuntimeState` when the backend is unavailable.
753pub fn cholesky(a: &EagerTensor) -> Result<EagerTensor> {
754 one_output(apply_linalg_eager(LinalgOp::Cholesky, &[a])?, "cholesky")
755}
756
757/// Hermitian eigenvalue decomposition for eager tensors.
758///
759/// # Examples
760///
761/// ```rust
762/// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
763/// use tenferro_linalg::EagerTensorLinalgExt;
764///
765/// let ctx = EagerRuntime::new()?;
766/// let a = EagerTensor::from_tensor_in(
767/// Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 0.0, 0.0, 3.0]).unwrap(),
768/// ctx,
769/// ).unwrap();
770/// let (values, vectors) = a.eigh()?;
771/// assert_eq!(values.shape(), &[2]);
772/// assert_eq!(vectors.shape(), &[2, 2]);
773/// # Ok::<(), tenferro_ad::Error>(())
774/// ```
775///
776/// # Errors
777///
778/// Returns `Error::Validation` for a non-square or invalid-rank input,
779/// `Error::Extension` for an unsupported dtype or numerical non-convergence,
780/// and `Error::RuntimeState` when the backend is unavailable.
781pub fn eigh(a: &EagerTensor) -> Result<(EagerTensor, EagerTensor)> {
782 eigh_with_options(a, EighOptions::default())
783}
784
785/// Hermitian eigenvalue decomposition for eager tensors with explicit options.
786///
787/// `derivative_eps` regularizes derivative formulas for repeated or nearly
788/// repeated eigenvalues. It is not a backend eigensolver tolerance.
789///
790/// # Examples
791///
792/// ```rust
793/// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
794/// use tenferro_linalg::{EagerTensorLinalgExt, EighGauge, EighOptions};
795///
796/// let ctx = EagerRuntime::new()?;
797/// let a = EagerTensor::from_tensor_in(
798/// Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 0.0, 0.0, 3.0]).unwrap(),
799/// ctx,
800/// ).unwrap();
801/// let (values, vectors) = a
802/// .eigh_with_options(
803/// EighOptions::default()
804/// .gauge(EighGauge::CanonicalPivot)
805/// .derivative_eps(1.0e-10),
806/// )?;
807/// assert_eq!(values.shape(), &[2]);
808/// assert_eq!(vectors.shape(), &[2, 2]);
809/// # Ok::<(), tenferro_ad::Error>(())
810/// ```
811///
812/// # Errors
813///
814/// Returns `Error::Validation` for an invalid rank, shape, or
815/// `derivative_eps`, `Error::Extension` for unsupported dtypes or numerical
816/// non-convergence, and `Error::Internal` for an output-count violation.
817pub fn eigh_with_options(
818 a: &EagerTensor,
819 options: EighOptions,
820) -> Result<(EagerTensor, EagerTensor)> {
821 validate_derivative_eps("eigh_with_options", options.derivative_eps)?;
822 two_outputs(
823 apply_linalg_eager(
824 LinalgOp::Eigh {
825 derivative_eps: options.derivative_eps,
826 gauge: options.gauge,
827 },
828 &[a],
829 )?,
830 "eigh",
831 )
832}
833
834/// General eigenvalue decomposition for eager tensors.
835///
836/// # Examples
837///
838/// ```rust
839/// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
840/// use tenferro_linalg::EagerTensorLinalgExt;
841///
842/// let ctx = EagerRuntime::new()?;
843/// let a = EagerTensor::from_tensor_in(
844/// Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 0.0, 0.0, 3.0]).unwrap(),
845/// ctx,
846/// ).unwrap();
847/// let (values, vectors) = a.eig()?;
848/// assert_eq!(values.shape(), &[2]);
849/// assert_eq!(vectors.shape(), &[2, 2]);
850/// # Ok::<(), tenferro_ad::Error>(())
851/// ```
852///
853/// # Errors
854///
855/// Returns `Error::Validation` for a non-square or invalid-rank input,
856/// `Error::Extension` for an unsupported dtype or numerical non-convergence,
857/// and `Error::RuntimeState` when the backend is unavailable.
858pub fn eig(a: &EagerTensor) -> Result<(EagerTensor, EagerTensor)> {
859 two_outputs(
860 apply_linalg_eager(
861 LinalgOp::Eig {
862 input_dtype: a.dtype(),
863 },
864 &[a],
865 )?,
866 "eig",
867 )
868}
869
870/// Triangular solve for eager tensors.
871///
872/// # Examples
873///
874/// ```rust
875/// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
876/// use tenferro_linalg::EagerTensorLinalgExt;
877///
878/// let ctx = EagerRuntime::new()?;
879/// let a = EagerTensor::from_tensor_in(
880/// Tensor::from_vec_col_major(vec![2, 2], vec![2.0_f64, 0.0, 1.0, 3.0]).unwrap(),
881/// ctx.clone(),
882/// ).unwrap();
883/// let b = EagerTensor::from_tensor_in(
884/// Tensor::from_vec_col_major(vec![2, 1], vec![2.0_f64, 7.0]).unwrap(),
885/// ctx,
886/// ).unwrap();
887/// let x = a.triangular_solve(&b, true, true, false, false)?;
888/// assert_eq!(x.shape(), &[2, 1]);
889/// # Ok::<(), tenferro_ad::Error>(())
890/// ```
891///
892/// # Errors
893///
894/// Returns `Error::Validation` for incompatible matrix, batch, or dtype
895/// metadata, `Error::Extension` for an unsupported dtype or singular system,
896/// and `Error::RuntimeState` when the backend is unavailable.
897pub fn triangular_solve(
898 a: &EagerTensor,
899 b: &EagerTensor,
900 left_side: bool,
901 lower: bool,
902 transpose_a: bool,
903 unit_diagonal: bool,
904) -> Result<EagerTensor> {
905 one_output(
906 apply_linalg_eager(
907 LinalgOp::TriangularSolve {
908 left_side,
909 lower,
910 transpose_a,
911 unit_diagonal,
912 },
913 &[a, b],
914 )?,
915 "triangular_solve",
916 )
917}
918
919fn one_output(outputs: Vec<EagerTensor>, name: &str) -> Result<EagerTensor> {
920 let mut outputs = outputs.into_iter();
921 match (outputs.next(), outputs.next()) {
922 (Some(output), None) => Ok(output),
923 _ => Err(Error::Internal(format!(
924 "{name} eager op returned an unexpected number of outputs"
925 ))),
926 }
927}
928
929fn two_outputs(outputs: Vec<EagerTensor>, name: &str) -> Result<(EagerTensor, EagerTensor)> {
930 let mut outputs = outputs.into_iter();
931 match (outputs.next(), outputs.next(), outputs.next()) {
932 (Some(lhs), Some(rhs), None) => Ok((lhs, rhs)),
933 _ => Err(Error::Internal(format!(
934 "{name} eager op returned an unexpected number of outputs"
935 ))),
936 }
937}