Skip to main content

tensor4all_tensorbackend/
backend.rs

1//! Backend dispatch helpers for linear algebra operations.
2//!
3//! This module keeps tensor4all's typed factorization entry points thin while
4//! routing the actual work through the shared tenferro CPU backend.
5
6use anyhow::{anyhow, Result};
7use num_complex::{Complex32, Complex64, ComplexFloat};
8use tenferro::{DType, Tensor, TensorScalar, TensorSessionOpsExt, TypedTensor};
9use tenferro_linalg::TensorLinalgExt;
10
11use crate::context::with_default_session;
12use crate::matrix::Matrix;
13
14/// Result of SVD decomposition `A = U * diag(S) * Vt`.
15/// The singular values are stored in a real-valued typed tensor, even when the
16/// input matrix is complex.
17/// # Examples
18/// ```
19/// use tensor4all_tensorbackend::svd_backend;
20/// use tenferro::TypedTensor;
21/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
22/// let a = TypedTensor::<f64>::from_vec_col_major(vec![2, 2], vec![1.0, 0.0, 0.0, 2.0])?;
23/// let result = svd_backend(&a)?;
24/// assert_eq!(result.u().shape(), &[2, 2]);
25/// assert_eq!(result.s().shape(), &[2]);
26/// assert_eq!(result.vt().shape(), &[2, 2]);
27/// # Ok(())
28/// # }
29/// ```
30#[derive(Debug)]
31pub struct SvdResult<T: TensorScalar> {
32    u: TypedTensor<T>,
33    s: TypedTensor<T::Real>,
34    vt: TypedTensor<T>,
35}
36
37/// Result of complete-pivoting LU decomposition `P A Q^T = L U`.
38/// The parity output from tenferro is intentionally omitted because current
39/// tensor4all callers only need the permutation matrices and the upper
40/// triangular factor for pivot selection.
41#[derive(Debug)]
42pub struct FullPivLuResult<T: TensorScalar> {
43    p: TypedTensor<T>,
44    l: TypedTensor<T>,
45    u: TypedTensor<T>,
46    q: TypedTensor<T>,
47}
48
49fn clone_linalg_tensor<T: TensorScalar>(tensor: &TypedTensor<T>) -> TypedTensor<T> {
50    crate::require_invariant(tensor.duplicate(), "linalg result duplication failed")
51}
52
53impl<T: TensorScalar> SvdResult<T> {
54    /// Borrow the left singular vectors.
55    pub fn u(&self) -> &TypedTensor<T> {
56        &self.u
57    }
58
59    /// Borrow the singular values.
60    pub fn s(&self) -> &TypedTensor<T::Real> {
61        &self.s
62    }
63
64    /// Borrow the transposed right singular vectors.
65    pub fn vt(&self) -> &TypedTensor<T> {
66        &self.vt
67    }
68
69    /// Consume the decomposition into `(U, S, Vt)`.
70    pub fn into_parts(self) -> (TypedTensor<T>, TypedTensor<T::Real>, TypedTensor<T>) {
71        (self.u, self.s, self.vt)
72    }
73}
74
75impl<T: TensorScalar> FullPivLuResult<T> {
76    /// Borrow the left permutation matrix.
77    pub fn p(&self) -> &TypedTensor<T> {
78        &self.p
79    }
80
81    /// Borrow the lower triangular factor.
82    pub fn l(&self) -> &TypedTensor<T> {
83        &self.l
84    }
85
86    /// Borrow the upper triangular factor.
87    pub fn u(&self) -> &TypedTensor<T> {
88        &self.u
89    }
90
91    /// Borrow the right permutation matrix.
92    pub fn q(&self) -> &TypedTensor<T> {
93        &self.q
94    }
95
96    /// Consume the decomposition into `(P, L, U, Q)`.
97    pub fn into_parts(
98        self,
99    ) -> (
100        TypedTensor<T>,
101        TypedTensor<T>,
102        TypedTensor<T>,
103        TypedTensor<T>,
104    ) {
105        (self.p, self.l, self.u, self.q)
106    }
107}
108
109impl<T: TensorScalar> Clone for SvdResult<T> {
110    fn clone(&self) -> Self {
111        Self {
112            u: clone_linalg_tensor(&self.u),
113            s: clone_linalg_tensor(&self.s),
114            vt: clone_linalg_tensor(&self.vt),
115        }
116    }
117}
118
119impl<T: TensorScalar> Clone for FullPivLuResult<T> {
120    fn clone(&self) -> Self {
121        Self {
122            p: clone_linalg_tensor(&self.p),
123            l: clone_linalg_tensor(&self.l),
124            u: clone_linalg_tensor(&self.u),
125            q: clone_linalg_tensor(&self.q),
126        }
127    }
128}
129
130/// Result of complete-pivoting LU decomposition on [`Matrix`] values.
131/// This is the matrix-shaped counterpart of [`FullPivLuResult`]. It exists so
132/// downstream crates can use backend linalg without hand-writing
133/// `TypedTensor` conversion code.
134/// # Examples
135/// ```
136/// use tensor4all_tensorbackend::{from_vec2d, full_piv_lu_matrix};
137/// let matrix = from_vec2d(vec![vec![0.0_f64, 1.0], vec![2.0, 3.0]]);
138/// let factors = full_piv_lu_matrix(&matrix).unwrap();
139/// assert_eq!(factors.u.nrows(), 2);
140/// assert_eq!(factors.u.ncols(), 2);
141/// ```
142#[derive(Debug, Clone)]
143pub struct FullPivLuMatrixResult<T> {
144    /// Left permutation matrix.
145    pub p: Matrix<T>,
146    /// Lower triangular factor.
147    pub l: Matrix<T>,
148    /// Upper triangular factor.
149    pub u: Matrix<T>,
150    /// Right permutation matrix.
151    pub q: Matrix<T>,
152}
153
154/// Scalar bound accepted by tensor4all's typed linalg wrappers.
155pub trait BackendLinalgScalar: TensorScalar {}
156
157/// Scalar types that can solve `T * P = Pi1` via a right full-pivoting LU
158/// solve on the tenferro backend.
159///
160/// This is the foundational seam for matrix cross-interpolation (CI)
161/// materialization: it lets an algorithm layer ask for the backend's
162/// full-pivot LU solve without depending on any higher crate. The four
163/// supported scalar types (f32, f64, Complex32, Complex64) are implemented
164/// here; `f32`/`Complex32` inputs are solved in double precision by the
165/// backend and converted back.
166///
167/// # Errors
168///
169/// Returns an error when the pivot matrix is not square, the shapes are
170/// incompatible, or the backend solve fails.
171pub trait FullPivLuScalar: BackendLinalgScalar {
172    /// Solve `T * P = Pi1` for `T`, where `P` is the pivot matrix (column-major).
173    ///
174    /// # Errors
175    ///
176    /// Returns a [`BackendLinalgError`] when the pivot matrix is not square
177    /// (a shape mismatch), when the shapes are incompatible (`lhs_cols !=
178    /// pivot_rows`, a shape mismatch), or when the backend full-pivot LU solve
179    /// fails (a backend failure).
180    fn solve_right_full_piv_lu(
181        lhs_values: &[Self],
182        lhs_rows: usize,
183        lhs_cols: usize,
184        pivot_values: &[Self],
185        pivot_rows: usize,
186        pivot_cols: usize,
187    ) -> std::result::Result<Vec<Self>, BackendLinalgError>;
188}
189
190macro_rules! impl_full_piv_lu_scalar {
191    ($t:ty) => {
192        impl FullPivLuScalar for $t {
193            fn solve_right_full_piv_lu(
194                lhs_values: &[Self],
195                lhs_rows: usize,
196                lhs_cols: usize,
197                pivot_values: &[Self],
198                pivot_rows: usize,
199                pivot_cols: usize,
200            ) -> std::result::Result<Vec<Self>, BackendLinalgError> {
201                if pivot_rows != pivot_cols {
202                    return Err(BackendLinalgError::from(anyhow::anyhow!(
203                        "full-pivot solve requires a square pivot matrix, got {}x{}",
204                        pivot_rows,
205                        pivot_cols
206                    )));
207                }
208                if lhs_cols != pivot_rows {
209                    return Err(BackendLinalgError::from(anyhow::anyhow!(
210                        "cannot solve T * P = Pi1 with Pi1 shape {}x{} and P shape {}x{}",
211                        lhs_rows,
212                        lhs_cols,
213                        pivot_rows,
214                        pivot_cols
215                    )));
216                }
217
218                let lhs_t = transpose_column_major(lhs_values, lhs_rows, lhs_cols);
219                let pivot_t = transpose_column_major(pivot_values, pivot_rows, pivot_cols);
220                let pivot_tensor = tenferro_tensor::Tensor::from_vec_col_major(
221                    vec![pivot_cols, pivot_rows],
222                    pivot_t,
223                )
224                .map_err(|e| BackendLinalgError::from(anyhow::Error::new(e)))?;
225                let lhs_tensor =
226                    tenferro_tensor::Tensor::from_vec_col_major(vec![lhs_cols, lhs_rows], lhs_t)
227                        .map_err(|e| BackendLinalgError::from(anyhow::Error::new(e)))?;
228                let solved_t = with_default_session(|session| {
229                    pivot_tensor.full_piv_lu_solve(&lhs_tensor, session)
230                })
231                .map_err(|e| {
232                    BackendLinalgError::from(anyhow::anyhow!("full_piv_lu_solve failed: {e}"))
233                })?;
234
235                let solved_t_values = solved_t.as_slice::<Self>().map_err(|e| {
236                    BackendLinalgError::from(anyhow::anyhow!(
237                        "full_piv_lu_solve returned unexpected dtype: {e}"
238                    ))
239                })?;
240                Ok(transpose_column_major(solved_t_values, lhs_cols, lhs_rows))
241            }
242        }
243    };
244}
245
246impl_full_piv_lu_scalar!(f32);
247impl_full_piv_lu_scalar!(f64);
248impl_full_piv_lu_scalar!(num_complex::Complex32);
249impl_full_piv_lu_scalar!(num_complex::Complex64);
250
251/// Transpose a column-major flat buffer.
252fn transpose_column_major<T: Copy + num_traits::Zero>(
253    values: &[T],
254    nrows: usize,
255    ncols: usize,
256) -> Vec<T> {
257    let mut out = vec![T::zero(); nrows * ncols];
258    for col in 0..ncols {
259        for row in 0..nrows {
260            out[col + ncols * row] = values[row + nrows * col];
261        }
262    }
263    out
264}
265
266impl<T: TensorScalar> BackendLinalgScalar for T {}
267
268/// Scalar types supported by [`solve_matrix`].
269/// `f64` and `Complex64` are solved directly. `f32` and `Complex32` are
270/// promoted to the corresponding 64-bit dtype for the backend solve and then
271/// converted back, because the current tenferro CPU LU solve is double
272/// precision only.
273/// # Examples
274/// ```
275/// use tensor4all_tensorbackend::{from_vec2d, solve_matrix};
276/// let a = from_vec2d(vec![vec![2.0_f32, 1.0], vec![1.0, 2.0]]);
277/// let b = from_vec2d(vec![vec![1.0_f32], vec![0.0]]);
278/// let x = solve_matrix(&a, &b).unwrap();
279/// assert!((x[[0, 0]] - 2.0 / 3.0).abs() < 1.0e-6);
280/// ```
281pub trait MatrixSolveScalar: BackendLinalgScalar + crate::matrix::MatrixScalar {
282    #[doc(hidden)]
283    fn solve_matrix_impl(a: &Matrix<Self>, b: &Matrix<Self>) -> Result<Matrix<Self>>;
284
285    #[doc(hidden)]
286    fn solve_matrix_owned_impl(a: Matrix<Self>, b: Matrix<Self>) -> Result<Matrix<Self>> {
287        Self::solve_matrix_impl(&a, &b)
288    }
289}
290
291/// Error returned by the CPU backend linear-algebra dispatch helpers.
292///
293/// Wraps the underlying tenferro or shape diagnostic, preserving its source
294/// chain.
295///
296/// # Remedies
297/// - Shape/dtype mismatch: verify matrix dims and scalar dtypes against the
298///   operation contract before calling.
299/// - Singular or invalid input: check matrix conditioning and finite values
300///   before solve/factorization operations.
301/// - Backend failure: the wrapped source chain identifies the failing stage.
302#[derive(Debug, thiserror::Error)]
303#[error("backend linear algebra failed: {source}")]
304pub struct BackendLinalgError {
305    /// Original backend or shape diagnostic.
306    #[source]
307    pub source: anyhow::Error,
308}
309
310impl From<anyhow::Error> for BackendLinalgError {
311    fn from(source: anyhow::Error) -> Self {
312        Self { source }
313    }
314}
315
316/// Scalar types supported by [`triangular_solve_matrix`].
317/// `f64` and `Complex64` are solved directly. `f32` and `Complex32` are
318/// promoted to the corresponding 64-bit dtype for the backend solve and then
319/// converted back, because the current tenferro CPU triangular solve is double
320/// precision only.
321/// # Examples
322/// ```
323/// use tensor4all_tensorbackend::{from_vec2d, triangular_solve_matrix};
324/// let a = from_vec2d(vec![vec![2.0_f64, 0.0], vec![1.0, 3.0]]);
325/// let b = from_vec2d(vec![vec![2.0_f64], vec![7.0]]);
326/// let x = triangular_solve_matrix(&a, &b, true, true, false, false).unwrap();
327/// assert!((x[[0, 0]] - 1.0).abs() < 1.0e-12);
328/// assert!((x[[1, 0]] - 2.0).abs() < 1.0e-12);
329/// ```
330pub trait MatrixTriangularSolveScalar: BackendLinalgScalar + crate::matrix::MatrixScalar {
331    #[doc(hidden)]
332    fn triangular_solve_matrix_impl(
333        a: &Matrix<Self>,
334        b: &Matrix<Self>,
335        left_side: bool,
336        lower: bool,
337        transpose_a: bool,
338        unit_diagonal: bool,
339    ) -> Result<Matrix<Self>>;
340
341    #[doc(hidden)]
342    fn triangular_solve_matrix_owned_impl(
343        a: Matrix<Self>,
344        b: Matrix<Self>,
345        left_side: bool,
346        lower: bool,
347        transpose_a: bool,
348        unit_diagonal: bool,
349    ) -> Result<Matrix<Self>> {
350        Self::triangular_solve_matrix_impl(&a, &b, left_side, lower, transpose_a, unit_diagonal)
351    }
352}
353
354/// Small-matrix diagnostics produced by the successive randomized compression
355/// stopping estimator.
356///
357/// `error` is the Appendix C randomized residual estimate and `norm` is the
358/// corresponding Frobenius norm estimate. Both values use the sketch width as
359/// their normalization factor.
360///
361/// # Examples
362///
363/// ```
364/// use tensor4all_tensorbackend::{src_error_estimate, Matrix};
365///
366/// let r = Matrix::from_col_major_vec(1, 1, vec![2.0_f64]);
367/// let estimate = src_error_estimate(&r).unwrap();
368/// assert!((estimate.error - 2.0).abs() < 1.0e-12);
369/// assert!((estimate.norm - 2.0).abs() < 1.0e-12);
370/// ```
371#[derive(Debug, Clone, Copy, PartialEq)]
372pub struct SrcErrorEstimate {
373    /// Estimated residual magnitude from the inverse-adjoint QR factor.
374    pub error: f64,
375    /// Estimated norm of the sketched tensor from the QR factor.
376    pub norm: f64,
377}
378
379/// Compute the Appendix C SRC error and norm estimates from an upper-triangular
380/// QR factor `R`.
381///
382/// Provenance: the formulas are Eq. (err-est) and Eq. (norm-est) in Appendix C
383/// of Camaño--Epperly--Tropp, [arXiv:2504.06475](https://arxiv.org/abs/2504.06475),
384/// cross-checked against `chriscamano/RandomMPOMPS/code/tensornetwork/incrementalqr.cpp::get_error_estimate`
385/// (lines 106--119). The use of actual `R` plus an `R†` solve is an equivalent
386/// representation derived in the audit; it is not a literal port of the
387/// author's inverse-`R` storage.
388///
389/// The helper explicitly builds `R†` before solving `R† G = I`, so complex
390/// inputs use the Hermitian adjoint rather than a plain transpose. The solve is
391/// delegated to the configured tensor4all backend and is restricted to the
392/// small sketch matrix; no general dense inverse routine is used.
393///
394/// # Errors
395///
396/// Returns [`BackendLinalgError`] when `r` is empty, non-square,
397/// non-triangular, singular, or contains non-finite values, or when the backend
398/// triangular solve fails.
399///
400/// # Examples
401///
402/// ```
403/// use tensor4all_tensorbackend::{src_error_estimate, Matrix};
404///
405/// let r = Matrix::from_col_major_vec(2, 2, vec![2.0_f64, 0.0, 1.0, 3.0]);
406/// let estimate = src_error_estimate(&r).unwrap();
407/// assert!(estimate.error.is_finite());
408/// assert!(estimate.norm.is_finite());
409/// ```
410pub fn src_error_estimate<T>(
411    r: &Matrix<T>,
412) -> std::result::Result<SrcErrorEstimate, BackendLinalgError>
413where
414    T: MatrixTriangularSolveScalar + ComplexFloat,
415{
416    let inverse_adjoint = src_inverse_adjoint(r)?;
417    src_error_estimate_from_inverse_adjoint(r, &inverse_adjoint)
418}
419
420/// Compute the Appendix C SRC estimates from a general square sketch factor.
421///
422/// This variant is equivalent to [`src_error_estimate`] but uses a general
423/// solve for factors whose columns have been restored from pivoted QR order.
424/// The matrix is a small SRC sketch factor, not a full tensor materialization.
425///
426/// # Errors
427///
428/// Returns [`BackendLinalgError`] when `factor` is empty, non-square, singular,
429/// contains non-finite values, or when the configured general solve fails.
430///
431/// # Examples
432///
433/// ```
434/// use tensor4all_tensorbackend::{src_error_estimate_general, Matrix};
435///
436/// let factor = Matrix::from_col_major_vec(2, 2, vec![0.0_f64, 3.0, 2.0, 1.0]);
437/// let estimate = src_error_estimate_general(&factor).unwrap();
438/// assert!(estimate.error.is_finite());
439/// assert!((estimate.norm - (14.0_f64 / 2.0).sqrt()).abs() < 1.0e-12);
440/// ```
441pub fn src_error_estimate_general<T>(
442    factor: &Matrix<T>,
443) -> std::result::Result<SrcErrorEstimate, BackendLinalgError>
444where
445    T: MatrixSolveScalar + ComplexFloat,
446{
447    let nrows = factor.nrows();
448    let ncols = factor.ncols();
449    if nrows != ncols {
450        return Err(BackendLinalgError::from(anyhow!(
451            "SRC estimator requires a square factor, got {nrows}x{ncols}"
452        )));
453    }
454    if nrows == 0 {
455        return Err(BackendLinalgError::from(anyhow!(
456            "SRC estimator requires a non-empty factor"
457        )));
458    }
459    if factor
460        .as_col_major_slice()
461        .iter()
462        .any(|value| !value.matrix_abs_sq().is_finite())
463    {
464        return Err(BackendLinalgError::from(anyhow!(
465            "SRC estimator requires finite entries in its factor"
466        )));
467    }
468    let mut adjoint = Matrix::zeros(nrows, ncols);
469    let mut identity = Matrix::zeros(nrows, ncols);
470    for col in 0..ncols {
471        for row in 0..nrows {
472            adjoint[[row, col]] = factor[[col, row]].conj();
473        }
474        identity[[col, col]] = T::one();
475    }
476    let inverse_adjoint = solve_matrix(&adjoint, &identity).map_err(|error| {
477        BackendLinalgError::from(anyhow!("SRC inverse-adjoint general solve failed: {error}"))
478    })?;
479    src_error_estimate_from_inverse_adjoint(factor, &inverse_adjoint)
480}
481
482/// Compute the inverse adjoint `R^{-†}` used by the Appendix C estimator.
483///
484/// This is crate-visible so incremental QR can initialize the stored
485/// estimator state once and then update it with the block formula from
486/// Appendix C.3 instead of solving the same triangular system after every
487/// appended sketch block.
488pub(crate) fn src_inverse_adjoint<T>(
489    r: &Matrix<T>,
490) -> std::result::Result<Matrix<T>, BackendLinalgError>
491where
492    T: MatrixTriangularSolveScalar + ComplexFloat,
493{
494    let nrows = r.nrows();
495    let ncols = r.ncols();
496    if nrows != ncols {
497        return Err(BackendLinalgError::from(anyhow!(
498            "SRC estimator requires a square R, got {nrows}x{ncols}"
499        )));
500    }
501    if nrows == 0 {
502        return Err(BackendLinalgError::from(anyhow!(
503            "SRC estimator requires a non-empty R"
504        )));
505    }
506
507    // Only the diagonal is checked here; the full-R Frobenius-norm
508    // finiteness check lives solely in `src_error_estimate_from_inverse_adjoint`,
509    // which every `src_error_estimate` call already runs immediately after
510    // this function returns. Duplicating that O(rank^2) accumulation here
511    // would recompute the identical sum for nothing on SRC's adaptive
512    // stopping-test hot path.
513    for col in 0..ncols {
514        let diagonal_sq = r[[col, col]].matrix_abs_sq();
515        if !diagonal_sq.is_finite() || diagonal_sq == 0.0 {
516            return Err(BackendLinalgError::from(anyhow!(
517                "SRC estimator requires a finite, nonzero diagonal in R at ({col}, {col})"
518            )));
519        }
520        for row in col + 1..nrows {
521            if r[[row, col]].matrix_abs_sq() != 0.0 {
522                return Err(BackendLinalgError::from(anyhow!(
523                    "SRC triangular estimator requires upper-triangular R; entry ({row}, {col}) is nonzero"
524                )));
525            }
526        }
527    }
528
529    let mut adjoint = Matrix::zeros(nrows, ncols);
530    for col in 0..ncols {
531        for row in 0..nrows {
532            adjoint[[row, col]] = r[[col, row]].conj();
533        }
534    }
535    let mut identity = Matrix::zeros(nrows, ncols);
536    for diagonal in 0..nrows {
537        identity[[diagonal, diagonal]] = T::one();
538    }
539
540    let inverse_adjoint = triangular_solve_matrix(&adjoint, &identity, true, true, false, false)
541        .map_err(|error| {
542            BackendLinalgError::from(anyhow!(
543                "SRC inverse-adjoint triangular solve failed: {error}"
544            ))
545        })?;
546    Ok(inverse_adjoint)
547}
548
549/// Evaluate the Appendix C estimator from a previously computed `R^{-†}`.
550///
551/// The inverse-adjoint argument is intentionally separate from
552/// [`src_error_estimate`] so incremental QR can reuse its updated triangular
553/// solve state. This helper performs only norm accumulation and validation.
554pub(crate) fn src_error_estimate_from_inverse_adjoint<T>(
555    r: &Matrix<T>,
556    inverse_adjoint: &Matrix<T>,
557) -> std::result::Result<SrcErrorEstimate, BackendLinalgError>
558where
559    T: crate::matrix::MatrixScalar + ComplexFloat,
560{
561    let nrows = r.nrows();
562    let ncols = r.ncols();
563    if nrows != ncols || inverse_adjoint.nrows() != nrows || inverse_adjoint.ncols() != ncols {
564        return Err(BackendLinalgError::from(anyhow!(
565            "SRC estimator requires matching square R and inverse-adjoint factors"
566        )));
567    }
568    if nrows == 0 {
569        return Err(BackendLinalgError::from(anyhow!(
570            "SRC estimator requires a non-empty R"
571        )));
572    }
573
574    let mut norm_sq = 0.0_f64;
575    for value in r.as_col_major_slice() {
576        norm_sq += value.matrix_abs_sq();
577    }
578    if !norm_sq.is_finite() {
579        return Err(BackendLinalgError::from(anyhow!(
580            "SRC estimator requires finite entries in R"
581        )));
582    }
583    let mut inverse_column_error_sq = 0.0_f64;
584    for col in 0..ncols {
585        let column_norm_sq = (0..nrows)
586            .map(|row| inverse_adjoint[[row, col]].matrix_abs_sq())
587            .sum::<f64>();
588        if !column_norm_sq.is_finite() || column_norm_sq == 0.0 {
589            return Err(BackendLinalgError::from(anyhow!(
590                "SRC inverse-adjoint solve returned an invalid column norm at column {col}"
591            )));
592        }
593        inverse_column_error_sq += 1.0 / column_norm_sq;
594    }
595
596    let sketch_width = ncols as f64;
597    let error_sq = inverse_column_error_sq / sketch_width;
598    let norm_estimate_sq = norm_sq / sketch_width;
599    if !error_sq.is_finite() || !norm_estimate_sq.is_finite() {
600        return Err(BackendLinalgError::from(anyhow!(
601            "SRC estimator produced a non-finite estimate"
602        )));
603    }
604    Ok(SrcErrorEstimate {
605        error: error_sq.sqrt(),
606        norm: norm_estimate_sq.sqrt(),
607    })
608}
609
610fn solve_matrix_direct<T>(a: &Matrix<T>, b: &Matrix<T>) -> Result<Matrix<T>>
611where
612    T: BackendLinalgScalar + Copy,
613    Tensor: From<TypedTensor<T>>,
614{
615    solve_matrix_direct_owned(a.clone(), b.clone())
616}
617
618fn solve_matrix_direct_owned<T>(a: Matrix<T>, b: Matrix<T>) -> Result<Matrix<T>>
619where
620    T: BackendLinalgScalar + Copy,
621    Tensor: From<TypedTensor<T>>,
622{
623    let a_tensor: Tensor = a.into_typed_tensor().into();
624    let b_tensor: Tensor = b.into_typed_tensor().into();
625    let result = with_default_session(|session| a_tensor.solve(&b_tensor, session))
626        .map_err(|e| anyhow!("linear solve failed via tenferro-tensor: {e}"))?;
627    let x = try_into_typed_result::<T>("solve", result)?;
628    typed_tensor_to_matrix("solve", x)
629}
630
631fn triangular_solve_matrix_direct<T>(
632    a: &Matrix<T>,
633    b: &Matrix<T>,
634    left_side: bool,
635    lower: bool,
636    transpose_a: bool,
637    unit_diagonal: bool,
638) -> Result<Matrix<T>>
639where
640    T: BackendLinalgScalar + Copy,
641    Tensor: From<TypedTensor<T>>,
642{
643    triangular_solve_matrix_direct_owned(
644        a.clone(),
645        b.clone(),
646        left_side,
647        lower,
648        transpose_a,
649        unit_diagonal,
650    )
651}
652
653fn triangular_solve_matrix_direct_owned<T>(
654    a: Matrix<T>,
655    b: Matrix<T>,
656    left_side: bool,
657    lower: bool,
658    transpose_a: bool,
659    unit_diagonal: bool,
660) -> Result<Matrix<T>>
661where
662    T: BackendLinalgScalar + Copy,
663    Tensor: From<TypedTensor<T>>,
664{
665    let a_tensor: Tensor = a.into_typed_tensor().into();
666    let b_tensor: Tensor = b.into_typed_tensor().into();
667    let result = with_default_session(|session| {
668        a_tensor.triangular_solve(
669            &b_tensor,
670            left_side,
671            lower,
672            transpose_a,
673            unit_diagonal,
674            session,
675        )
676    })
677    .map_err(|e| anyhow!("triangular solve failed via tenferro-tensor: {e}"))?;
678    let x = try_into_typed_result::<T>("triangular_solve", result)?;
679    typed_tensor_to_matrix("triangular_solve", x)
680}
681
682impl MatrixSolveScalar for f64 {
683    fn solve_matrix_impl(a: &Matrix<Self>, b: &Matrix<Self>) -> Result<Matrix<Self>> {
684        solve_matrix_direct(a, b)
685    }
686
687    fn solve_matrix_owned_impl(a: Matrix<Self>, b: Matrix<Self>) -> Result<Matrix<Self>> {
688        solve_matrix_direct_owned(a, b)
689    }
690}
691
692impl MatrixTriangularSolveScalar for f64 {
693    fn triangular_solve_matrix_impl(
694        a: &Matrix<Self>,
695        b: &Matrix<Self>,
696        left_side: bool,
697        lower: bool,
698        transpose_a: bool,
699        unit_diagonal: bool,
700    ) -> Result<Matrix<Self>> {
701        triangular_solve_matrix_direct(a, b, left_side, lower, transpose_a, unit_diagonal)
702    }
703
704    fn triangular_solve_matrix_owned_impl(
705        a: Matrix<Self>,
706        b: Matrix<Self>,
707        left_side: bool,
708        lower: bool,
709        transpose_a: bool,
710        unit_diagonal: bool,
711    ) -> Result<Matrix<Self>> {
712        triangular_solve_matrix_direct_owned(a, b, left_side, lower, transpose_a, unit_diagonal)
713    }
714}
715
716impl MatrixSolveScalar for Complex64 {
717    fn solve_matrix_impl(a: &Matrix<Self>, b: &Matrix<Self>) -> Result<Matrix<Self>> {
718        solve_matrix_direct(a, b)
719    }
720
721    fn solve_matrix_owned_impl(a: Matrix<Self>, b: Matrix<Self>) -> Result<Matrix<Self>> {
722        solve_matrix_direct_owned(a, b)
723    }
724}
725
726impl MatrixTriangularSolveScalar for Complex64 {
727    fn triangular_solve_matrix_impl(
728        a: &Matrix<Self>,
729        b: &Matrix<Self>,
730        left_side: bool,
731        lower: bool,
732        transpose_a: bool,
733        unit_diagonal: bool,
734    ) -> Result<Matrix<Self>> {
735        triangular_solve_matrix_direct(a, b, left_side, lower, transpose_a, unit_diagonal)
736    }
737
738    fn triangular_solve_matrix_owned_impl(
739        a: Matrix<Self>,
740        b: Matrix<Self>,
741        left_side: bool,
742        lower: bool,
743        transpose_a: bool,
744        unit_diagonal: bool,
745    ) -> Result<Matrix<Self>> {
746        triangular_solve_matrix_direct_owned(a, b, left_side, lower, transpose_a, unit_diagonal)
747    }
748}
749
750impl MatrixSolveScalar for f32 {
751    fn solve_matrix_impl(a: &Matrix<Self>, b: &Matrix<Self>) -> Result<Matrix<Self>> {
752        let a64 = Matrix::from_col_major_vec(
753            a.nrows(),
754            a.ncols(),
755            a.as_col_major_slice()
756                .iter()
757                .map(|&value| value as f64)
758                .collect(),
759        );
760        let b64 = Matrix::from_col_major_vec(
761            b.nrows(),
762            b.ncols(),
763            b.as_col_major_slice()
764                .iter()
765                .map(|&value| value as f64)
766                .collect(),
767        );
768        let x64 = solve_matrix_direct(&a64, &b64)?;
769        Ok(Matrix::from_col_major_vec(
770            x64.nrows(),
771            x64.ncols(),
772            x64.as_col_major_slice()
773                .iter()
774                .map(|&value| value as f32)
775                .collect(),
776        ))
777    }
778}
779
780impl MatrixTriangularSolveScalar for f32 {
781    fn triangular_solve_matrix_impl(
782        a: &Matrix<Self>,
783        b: &Matrix<Self>,
784        left_side: bool,
785        lower: bool,
786        transpose_a: bool,
787        unit_diagonal: bool,
788    ) -> Result<Matrix<Self>> {
789        let a64 = Matrix::from_col_major_vec(
790            a.nrows(),
791            a.ncols(),
792            a.as_col_major_slice()
793                .iter()
794                .map(|&value| value as f64)
795                .collect(),
796        );
797        let b64 = Matrix::from_col_major_vec(
798            b.nrows(),
799            b.ncols(),
800            b.as_col_major_slice()
801                .iter()
802                .map(|&value| value as f64)
803                .collect(),
804        );
805        let x64 = triangular_solve_matrix_direct(
806            &a64,
807            &b64,
808            left_side,
809            lower,
810            transpose_a,
811            unit_diagonal,
812        )?;
813        Ok(Matrix::from_col_major_vec(
814            x64.nrows(),
815            x64.ncols(),
816            x64.as_col_major_slice()
817                .iter()
818                .map(|&value| value as f32)
819                .collect(),
820        ))
821    }
822}
823
824impl MatrixSolveScalar for Complex32 {
825    fn solve_matrix_impl(a: &Matrix<Self>, b: &Matrix<Self>) -> Result<Matrix<Self>> {
826        let a64 = Matrix::from_col_major_vec(
827            a.nrows(),
828            a.ncols(),
829            a.as_col_major_slice()
830                .iter()
831                .map(|&value| Complex64::new(value.re as f64, value.im as f64))
832                .collect(),
833        );
834        let b64 = Matrix::from_col_major_vec(
835            b.nrows(),
836            b.ncols(),
837            b.as_col_major_slice()
838                .iter()
839                .map(|&value| Complex64::new(value.re as f64, value.im as f64))
840                .collect(),
841        );
842        let x64 = solve_matrix_direct(&a64, &b64)?;
843        Ok(Matrix::from_col_major_vec(
844            x64.nrows(),
845            x64.ncols(),
846            x64.as_col_major_slice()
847                .iter()
848                .map(|&value| Complex32::new(value.re as f32, value.im as f32))
849                .collect(),
850        ))
851    }
852}
853
854impl MatrixTriangularSolveScalar for Complex32 {
855    fn triangular_solve_matrix_impl(
856        a: &Matrix<Self>,
857        b: &Matrix<Self>,
858        left_side: bool,
859        lower: bool,
860        transpose_a: bool,
861        unit_diagonal: bool,
862    ) -> Result<Matrix<Self>> {
863        let a64 = Matrix::from_col_major_vec(
864            a.nrows(),
865            a.ncols(),
866            a.as_col_major_slice()
867                .iter()
868                .map(|&value| Complex64::new(value.re as f64, value.im as f64))
869                .collect(),
870        );
871        let b64 = Matrix::from_col_major_vec(
872            b.nrows(),
873            b.ncols(),
874            b.as_col_major_slice()
875                .iter()
876                .map(|&value| Complex64::new(value.re as f64, value.im as f64))
877                .collect(),
878        );
879        let x64 = triangular_solve_matrix_direct(
880            &a64,
881            &b64,
882            left_side,
883            lower,
884            transpose_a,
885            unit_diagonal,
886        )?;
887        Ok(Matrix::from_col_major_vec(
888            x64.nrows(),
889            x64.ncols(),
890            x64.as_col_major_slice()
891                .iter()
892                .map(|&value| Complex32::new(value.re as f32, value.im as f32))
893                .collect(),
894        ))
895    }
896}
897
898fn tensor_scalar_dtype<T: TensorScalar>() -> DType {
899    T::dtype()
900}
901
902fn try_into_typed_result<T: TensorScalar>(
903    op: &'static str,
904    tensor: Tensor,
905) -> Result<TypedTensor<T>> {
906    let actual = tensor.dtype();
907    T::into_typed(tensor).map_err(|source| {
908        anyhow!(
909            "{op}: dtype mismatch lhs={actual:?} rhs={:?}: {source}",
910            tensor_scalar_dtype::<T>()
911        )
912    })
913}
914
915fn convert_for_typed<T: TensorScalar>(op: &'static str, tensor: Tensor) -> Result<TypedTensor<T>> {
916    let expected = tensor_scalar_dtype::<T>();
917    let tensor = if tensor.dtype() == expected {
918        tensor
919    } else {
920        with_default_session(|session| tensor.convert(expected, session))
921            .map_err(|e| anyhow!("{op}: dtype conversion to {expected:?} failed: {e}"))?
922    };
923    try_into_typed_result::<T>(op, tensor)
924}
925
926fn matrix_to_typed_tensor<T>(matrix: &Matrix<T>) -> TypedTensor<T>
927where
928    T: TensorScalar + Copy,
929{
930    crate::require_invariant(
931        TypedTensor::from_vec_col_major(
932            vec![matrix.nrows(), matrix.ncols()],
933            matrix.as_col_major_slice().to_vec(),
934        ),
935        "validated matrix rejected by tenferro",
936    )
937}
938
939fn typed_tensor_to_matrix<T>(op: &'static str, tensor: TypedTensor<T>) -> Result<Matrix<T>>
940where
941    T: TensorScalar + Copy,
942{
943    Matrix::try_from_typed_tensor(tensor).map_err(|err| anyhow!("{op}: {err}"))
944}
945
946fn require_host_linalg_tensor<T: TensorScalar>(
947    op: &'static str,
948    tensor: TypedTensor<T>,
949) -> Result<TypedTensor<T>> {
950    tensor
951        .host_data()
952        .map_err(|error| anyhow!("{op}: result must be host-backed: {error}"))?;
953    Ok(tensor)
954}
955
956/// Compute a thin/economy SVD on a typed tensor.
957/// # Errors
958///
959/// Returns an error when the SVD fails (a backend or non-convergence
960/// /// failure).
961///
962pub fn svd_backend<T>(a: &TypedTensor<T>) -> std::result::Result<SvdResult<T>, BackendLinalgError>
963where
964    T: BackendLinalgScalar,
965{
966    let tensor = T::into_tensor(
967        a.shape().to_vec(),
968        a.host_data()
969            .map_err(|e| anyhow!("SVD input host access failed: {e}"))?
970            .to_vec(),
971    )
972    .map_err(|e| anyhow!("SVD input tensor construction failed: {e}"))?;
973    let (u, s, vt) = with_default_session(|session| tensor.svd(session))
974        .map_err(|e| anyhow!("SVD computation failed via tenferro-tensor: {e}"))?;
975    Ok(SvdResult {
976        u: require_host_linalg_tensor("svd", convert_for_typed::<T>("svd", u)?)?,
977        s: require_host_linalg_tensor("svd", convert_for_typed::<T::Real>("svd", s)?)?,
978        vt: require_host_linalg_tensor("svd", convert_for_typed::<T>("svd", vt)?)?,
979    })
980}
981
982/// Compute a thin/economy QR decomposition, consuming the input tensor so its
983/// column-major storage can be transferred to the backend without copying.
984/// # Errors
985///
986/// Returns an error when the QR fails (a backend or non-convergence
987/// /// failure).
988///
989pub fn qr_backend<T>(
990    a: TypedTensor<T>,
991) -> std::result::Result<(TypedTensor<T>, TypedTensor<T>), BackendLinalgError>
992where
993    T: BackendLinalgScalar,
994{
995    let (shape, data) = a
996        .into_vec_col_major()
997        .map_err(|e| anyhow!("QR input host access failed: {e}"))?;
998    let tensor = T::into_tensor(shape, data)
999        .map_err(|e| anyhow!("QR input tensor construction failed: {e}"))?;
1000    let (q, r) = with_default_session(|session| tensor.qr(session))
1001        .map_err(|e| anyhow!("QR computation failed via tenferro-tensor: {e}"))?;
1002    Ok((
1003        convert_for_typed::<T>("qr", q)?,
1004        convert_for_typed::<T>("qr", r)?,
1005    ))
1006}
1007
1008/// Solve `A X = B` with the configured tenferro backend.
1009/// # Errors
1010/// Returns an error when the input shapes or scalar dtype are invalid (a
1011/// shape or dtype mismatch) or the coefficient matrix is singular (a singular
1012/// failure).
1013pub fn solve_backend<T>(
1014    a: &TypedTensor<T>,
1015    b: &TypedTensor<T>,
1016) -> std::result::Result<TypedTensor<T>, BackendLinalgError>
1017where
1018    T: BackendLinalgScalar,
1019{
1020    let a_tensor = T::into_tensor(
1021        a.shape().to_vec(),
1022        a.host_data()
1023            .map_err(|e| anyhow!("solve input host access failed: {e}"))?
1024            .to_vec(),
1025    )
1026    .map_err(|e| anyhow!("solve lhs tensor construction failed: {e}"))?;
1027    let b_tensor = T::into_tensor(
1028        b.shape().to_vec(),
1029        b.host_data()
1030            .map_err(|e| anyhow!("solve rhs host access failed: {e}"))?
1031            .to_vec(),
1032    )
1033    .map_err(|e| anyhow!("solve rhs tensor construction failed: {e}"))?;
1034    let result = with_default_session(|session| a_tensor.solve(&b_tensor, session))
1035        .map_err(|e| anyhow!("linear solve failed via tenferro-tensor: {e}"))?;
1036    try_into_typed_result::<T>("solve", result).map_err(BackendLinalgError::from)
1037}
1038
1039/// Solve a triangular system with the configured tenferro backend.
1040/// If `left_side` is true, this solves `op(A) X = B`; otherwise it solves
1041/// `X op(A) = B`. `lower` selects the triangular half, `transpose_a` applies
1042/// a transpose to `A`, and `unit_diagonal` treats the diagonal of `A` as ones.
1043/// # Errors
1044///
1045/// Returns an error when the solve fails (a backend, singular, or shape
1046/// /// mismatch failure).
1047///
1048pub fn triangular_solve_backend<T>(
1049    a: &TypedTensor<T>,
1050    b: &TypedTensor<T>,
1051    left_side: bool,
1052    lower: bool,
1053    transpose_a: bool,
1054    unit_diagonal: bool,
1055) -> std::result::Result<TypedTensor<T>, BackendLinalgError>
1056where
1057    T: BackendLinalgScalar,
1058{
1059    let a_tensor = T::into_tensor(
1060        a.shape().to_vec(),
1061        a.host_data()
1062            .map_err(|e| anyhow!("triangular solve input host access failed: {e}"))?
1063            .to_vec(),
1064    )
1065    .map_err(|e| anyhow!("triangular solve lhs tensor construction failed: {e}"))?;
1066    let b_tensor = T::into_tensor(
1067        b.shape().to_vec(),
1068        b.host_data()
1069            .map_err(|e| anyhow!("triangular solve rhs host access failed: {e}"))?
1070            .to_vec(),
1071    )
1072    .map_err(|e| anyhow!("triangular solve rhs tensor construction failed: {e}"))?;
1073    let result = with_default_session(|session| {
1074        a_tensor.triangular_solve(
1075            &b_tensor,
1076            left_side,
1077            lower,
1078            transpose_a,
1079            unit_diagonal,
1080            session,
1081        )
1082    })
1083    .map_err(|e| anyhow!("triangular solve failed via tenferro-tensor: {e}"))?;
1084    try_into_typed_result::<T>("triangular_solve", result).map_err(BackendLinalgError::from)
1085}
1086
1087/// Solve `A X = B` for column-major [`Matrix`] values.
1088/// This routes the operation through the configured tenferro backend and keeps
1089/// matrix-to-tensor conversion centralized in `tensor4all-tensorbackend`.
1090/// # Errors
1091///
1092/// Returns an error when the input shapes or scalar dtype are invalid (a
1093/// /// shape or dtype mismatch) or the solve fails (a backend or singular
1094/// /// failure).
1095///
1096/// # Examples
1097/// ```
1098/// use tensor4all_tensorbackend::{from_vec2d, solve_matrix};
1099/// let a = from_vec2d(vec![vec![2.0_f64, 1.0], vec![1.0, 2.0]]);
1100/// let b = from_vec2d(vec![vec![1.0_f64], vec![0.0]]);
1101/// let x = solve_matrix(&a, &b).unwrap();
1102/// assert!((x[[0, 0]] - 2.0 / 3.0).abs() < 1.0e-12);
1103/// assert!((x[[1, 0]] + 1.0 / 3.0).abs() < 1.0e-12);
1104/// ```
1105pub fn solve_matrix<T>(
1106    a: &Matrix<T>,
1107    b: &Matrix<T>,
1108) -> std::result::Result<Matrix<T>, BackendLinalgError>
1109where
1110    T: MatrixSolveScalar,
1111{
1112    T::solve_matrix_impl(a, b).map_err(BackendLinalgError::from)
1113}
1114
1115/// Solve `A X = B` while consuming column-major [`Matrix`] values.
1116/// This routes the operation through the configured tenferro backend and reuses
1117/// the input buffers when constructing backend tensors for directly supported
1118/// scalar types.
1119/// # Errors
1120///
1121/// Returns an error when the input shapes or scalar dtype are invalid (a
1122/// /// shape or dtype mismatch) or the solve fails (a backend or singular
1123/// /// failure).
1124///
1125/// # Examples
1126/// ```
1127/// use tensor4all_tensorbackend::{from_vec2d, solve_matrix_owned};
1128/// let a = from_vec2d(vec![vec![2.0_f64, 1.0], vec![1.0, 2.0]]);
1129/// let b = from_vec2d(vec![vec![1.0_f64], vec![0.0]]);
1130/// let x = solve_matrix_owned(a, b).unwrap();
1131/// assert!((x[[0, 0]] - 2.0 / 3.0).abs() < 1.0e-12);
1132/// assert!((x[[1, 0]] + 1.0 / 3.0).abs() < 1.0e-12);
1133/// ```
1134pub fn solve_matrix_owned<T>(
1135    a: Matrix<T>,
1136    b: Matrix<T>,
1137) -> std::result::Result<Matrix<T>, BackendLinalgError>
1138where
1139    T: MatrixSolveScalar,
1140{
1141    T::solve_matrix_owned_impl(a, b).map_err(BackendLinalgError::from)
1142}
1143
1144/// Solve a triangular system for column-major [`Matrix`] values.
1145/// If `left_side` is true, this solves `op(A) X = B`; otherwise it solves
1146/// `X op(A) = B`. `lower` selects the triangular half, `transpose_a` applies
1147/// a transpose to `A`, and `unit_diagonal` treats the diagonal of `A` as ones.
1148/// # Errors
1149///
1150/// Returns an error when the input shapes or scalar dtype are invalid (a
1151/// /// shape or dtype mismatch), the triangular flags are invalid (an
1152/// /// invalid-configuration failure), or the solve fails (a backend or singular
1153/// /// failure).
1154///
1155/// # Examples
1156/// ```
1157/// use tensor4all_tensorbackend::{from_vec2d, triangular_solve_matrix};
1158/// let a = from_vec2d(vec![vec![2.0_f64, 1.0], vec![0.0, 3.0]]);
1159/// let b = from_vec2d(vec![vec![2.0_f64, 7.0]]);
1160/// let x = triangular_solve_matrix(&a, &b, false, false, false, false).unwrap();
1161/// assert!((x[[0, 0]] - 1.0).abs() < 1.0e-12);
1162/// assert!((x[[0, 1]] - 2.0).abs() < 1.0e-12);
1163/// ```
1164pub fn triangular_solve_matrix<T>(
1165    a: &Matrix<T>,
1166    b: &Matrix<T>,
1167    left_side: bool,
1168    lower: bool,
1169    transpose_a: bool,
1170    unit_diagonal: bool,
1171) -> std::result::Result<Matrix<T>, BackendLinalgError>
1172where
1173    T: MatrixTriangularSolveScalar,
1174{
1175    T::triangular_solve_matrix_impl(a, b, left_side, lower, transpose_a, unit_diagonal)
1176        .map_err(BackendLinalgError::from)
1177}
1178
1179/// Solve a triangular system while consuming column-major [`Matrix`] values.
1180/// If `left_side` is true, this solves `op(A) X = B`; otherwise it solves
1181/// `X op(A) = B`. `lower` selects the triangular half, `transpose_a` applies
1182/// a transpose to `A`, and `unit_diagonal` treats the diagonal of `A` as ones.
1183/// # Errors
1184///
1185/// Returns an error when the input shapes or scalar dtype are invalid (a
1186/// /// shape or dtype mismatch), the triangular flags are invalid (an
1187/// /// invalid-configuration failure), or the solve fails (a backend or singular
1188/// /// failure).
1189///
1190/// # Examples
1191/// ```
1192/// use tensor4all_tensorbackend::{from_vec2d, triangular_solve_matrix_owned};
1193/// let a = from_vec2d(vec![vec![2.0_f64, 0.0], vec![1.0, 3.0]]);
1194/// let b = from_vec2d(vec![vec![2.0_f64], vec![7.0]]);
1195/// let x = triangular_solve_matrix_owned(a, b, true, true, false, false).unwrap();
1196/// assert!((x[[0, 0]] - 1.0).abs() < 1.0e-12);
1197/// assert!((x[[1, 0]] - 2.0).abs() < 1.0e-12);
1198/// ```
1199pub fn triangular_solve_matrix_owned<T>(
1200    a: Matrix<T>,
1201    b: Matrix<T>,
1202    left_side: bool,
1203    lower: bool,
1204    transpose_a: bool,
1205    unit_diagonal: bool,
1206) -> std::result::Result<Matrix<T>, BackendLinalgError>
1207where
1208    T: MatrixTriangularSolveScalar,
1209{
1210    T::triangular_solve_matrix_owned_impl(a, b, left_side, lower, transpose_a, unit_diagonal)
1211        .map_err(BackendLinalgError::from)
1212}
1213
1214fn full_piv_lu_tensor<T>(
1215    tensor: Tensor,
1216) -> std::result::Result<FullPivLuResult<T>, BackendLinalgError>
1217where
1218    T: BackendLinalgScalar,
1219{
1220    let (p, l, u, q, _parity) = with_default_session(|session| tensor.full_piv_lu(session))
1221        .map_err(|e| anyhow!("complete-pivoting LU failed via tenferro-tensor: {e}"))?;
1222    Ok(FullPivLuResult {
1223        p: require_host_linalg_tensor("full_piv_lu", convert_for_typed::<T>("full_piv_lu", p)?)?,
1224        l: require_host_linalg_tensor("full_piv_lu", convert_for_typed::<T>("full_piv_lu", l)?)?,
1225        u: require_host_linalg_tensor("full_piv_lu", convert_for_typed::<T>("full_piv_lu", u)?)?,
1226        q: require_host_linalg_tensor("full_piv_lu", convert_for_typed::<T>("full_piv_lu", q)?)?,
1227    })
1228}
1229
1230/// Compute complete-pivoting LU with the configured tenferro backend.
1231/// # Errors
1232///
1233/// Returns an error when the LU factorization fails (a backend or
1234/// /// non-convergence failure).
1235///
1236pub fn full_piv_lu_backend<T>(
1237    a: &TypedTensor<T>,
1238) -> std::result::Result<FullPivLuResult<T>, BackendLinalgError>
1239where
1240    T: BackendLinalgScalar,
1241{
1242    let tensor = T::into_tensor(
1243        a.shape().to_vec(),
1244        a.host_data()
1245            .map_err(|e| anyhow!("LU input host access failed: {e}"))?
1246            .to_vec(),
1247    )
1248    .map_err(|e| anyhow!("LU input tensor construction failed: {e}"))?;
1249    full_piv_lu_tensor(tensor)
1250}
1251
1252/// Compute complete-pivoting LU for a column-major [`Matrix`].
1253/// This is a convenience wrapper over [`full_piv_lu_backend`] for callers that
1254/// use [`Matrix`] as their dense boundary type.
1255/// # Errors
1256///
1257/// Returns an error when the backend does not support the input dtype (a
1258/// /// dtype mismatch) or the factorization fails (a backend or
1259/// /// non-convergence failure).
1260///
1261/// # Examples
1262/// ```
1263/// use tensor4all_tensorbackend::{from_vec2d, full_piv_lu_matrix};
1264/// let matrix = from_vec2d(vec![vec![0.0_f64, 1.0], vec![2.0, 3.0]]);
1265/// let factors = full_piv_lu_matrix(&matrix).unwrap();
1266/// assert_eq!(factors.p.nrows(), 2);
1267/// assert_eq!(factors.q.ncols(), 2);
1268/// ```
1269pub fn full_piv_lu_matrix<T>(
1270    a: &Matrix<T>,
1271) -> std::result::Result<FullPivLuMatrixResult<T>, BackendLinalgError>
1272where
1273    T: BackendLinalgScalar + Copy,
1274{
1275    let tensor = matrix_to_typed_tensor(a);
1276    let decomp = full_piv_lu_backend(&tensor)?;
1277    Ok(FullPivLuMatrixResult {
1278        p: typed_tensor_to_matrix("full_piv_lu", decomp.p)?,
1279        l: typed_tensor_to_matrix("full_piv_lu", decomp.l)?,
1280        u: typed_tensor_to_matrix("full_piv_lu", decomp.u)?,
1281        q: typed_tensor_to_matrix("full_piv_lu", decomp.q)?,
1282    })
1283}
1284
1285/// Compute complete-pivoting LU for an owned column-major [`Matrix`].
1286///
1287/// This is the owned-buffer counterpart of [`full_piv_lu_matrix`]. It consumes
1288/// the matrix so its column-major buffer can be transferred to tenferro without
1289/// cloning the input before factorization.
1290/// The returned factors satisfy `P * A * Qᵀ = L * U` when interpreted as
1291/// column-major matrices.
1292///
1293/// # Errors
1294///
1295/// Returns [`BackendLinalgError`] when the input is not square (tenferro
1296/// reports an incompatible shape), the configured backend rejects the scalar
1297/// dtype, the complete-pivoting factorization fails, or a factor produced by
1298/// the backend cannot be converted back to a matrix.
1299///
1300/// # Examples
1301/// ```
1302/// use tensor4all_tensorbackend::{from_vec2d, full_piv_lu_matrix_owned, Matrix};
1303///
1304/// fn matmul(a: &Matrix<f64>, b: &Matrix<f64>) -> Matrix<f64> {
1305///     let mut out = Matrix::zeros(a.nrows(), b.ncols());
1306///     for col in 0..b.ncols() {
1307///         for k in 0..a.ncols() {
1308///             for row in 0..a.nrows() {
1309///                 out[[row, col]] += a[[row, k]] * b[[k, col]];
1310///             }
1311///         }
1312///     }
1313///     out
1314/// }
1315///
1316/// fn transpose(a: &Matrix<f64>) -> Matrix<f64> {
1317///     let mut out = Matrix::zeros(a.ncols(), a.nrows());
1318///     for col in 0..a.ncols() {
1319///         for row in 0..a.nrows() {
1320///             out[[col, row]] = a[[row, col]];
1321///         }
1322///     }
1323///     out
1324/// }
1325///
1326/// let matrix = from_vec2d(vec![vec![2.0_f64, 1.0], vec![1.0, 2.0]]);
1327/// let factors = full_piv_lu_matrix_owned(matrix.clone()).unwrap();
1328/// let lhs = matmul(&factors.p, &matmul(&matrix, &transpose(&factors.q)));
1329/// let rhs = matmul(&factors.l, &factors.u);
1330/// for row in 0..2 {
1331///     for col in 0..2 {
1332///         assert!((lhs[[row, col]] - rhs[[row, col]]).abs() < 1.0e-12);
1333///     }
1334/// }
1335/// ```
1336pub fn full_piv_lu_matrix_owned<T>(
1337    a: Matrix<T>,
1338) -> std::result::Result<FullPivLuMatrixResult<T>, BackendLinalgError>
1339where
1340    T: BackendLinalgScalar + Copy,
1341{
1342    let tensor = T::typed_tensor_into_tensor(a.into_typed_tensor());
1343    let decomp = full_piv_lu_tensor(tensor)?;
1344    Ok(FullPivLuMatrixResult {
1345        p: typed_tensor_to_matrix("full_piv_lu", decomp.p)?,
1346        l: typed_tensor_to_matrix("full_piv_lu", decomp.l)?,
1347        u: typed_tensor_to_matrix("full_piv_lu", decomp.u)?,
1348        q: typed_tensor_to_matrix("full_piv_lu", decomp.q)?,
1349    })
1350}
1351
1352#[cfg(test)]
1353mod tests;