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, CpuExecutionContext};
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    tensor.duplicate().unwrap_or_else(|error| {
51        crate::invariant_violated("linalg result duplication failed", error)
52    })
53}
54
55impl<T: TensorScalar> SvdResult<T> {
56    /// Borrow the left singular vectors.
57    pub fn u(&self) -> &TypedTensor<T> {
58        &self.u
59    }
60
61    /// Borrow the singular values.
62    pub fn s(&self) -> &TypedTensor<T::Real> {
63        &self.s
64    }
65
66    /// Borrow the transposed right singular vectors.
67    pub fn vt(&self) -> &TypedTensor<T> {
68        &self.vt
69    }
70
71    /// Consume the decomposition into `(U, S, Vt)`.
72    pub fn into_parts(self) -> (TypedTensor<T>, TypedTensor<T::Real>, TypedTensor<T>) {
73        (self.u, self.s, self.vt)
74    }
75}
76
77impl<T: TensorScalar> FullPivLuResult<T> {
78    /// Borrow the left permutation matrix.
79    pub fn p(&self) -> &TypedTensor<T> {
80        &self.p
81    }
82
83    /// Borrow the lower triangular factor.
84    pub fn l(&self) -> &TypedTensor<T> {
85        &self.l
86    }
87
88    /// Borrow the upper triangular factor.
89    pub fn u(&self) -> &TypedTensor<T> {
90        &self.u
91    }
92
93    /// Borrow the right permutation matrix.
94    pub fn q(&self) -> &TypedTensor<T> {
95        &self.q
96    }
97
98    /// Consume the decomposition into `(P, L, U, Q)`.
99    pub fn into_parts(
100        self,
101    ) -> (
102        TypedTensor<T>,
103        TypedTensor<T>,
104        TypedTensor<T>,
105        TypedTensor<T>,
106    ) {
107        (self.p, self.l, self.u, self.q)
108    }
109}
110
111impl<T: TensorScalar> Clone for SvdResult<T> {
112    fn clone(&self) -> Self {
113        Self {
114            u: clone_linalg_tensor(&self.u),
115            s: clone_linalg_tensor(&self.s),
116            vt: clone_linalg_tensor(&self.vt),
117        }
118    }
119}
120
121impl<T: TensorScalar> Clone for FullPivLuResult<T> {
122    fn clone(&self) -> Self {
123        Self {
124            p: clone_linalg_tensor(&self.p),
125            l: clone_linalg_tensor(&self.l),
126            u: clone_linalg_tensor(&self.u),
127            q: clone_linalg_tensor(&self.q),
128        }
129    }
130}
131
132/// Result of complete-pivoting LU decomposition on [`Matrix`] values.
133/// This is the matrix-shaped counterpart of [`FullPivLuResult`]. It exists so
134/// downstream crates can use backend linalg without hand-writing
135/// `TypedTensor` conversion code.
136/// # Examples
137/// ```
138/// use tensor4all_tensorbackend::{from_vec2d, full_piv_lu_matrix};
139/// let matrix = from_vec2d(vec![vec![0.0_f64, 1.0], vec![2.0, 3.0]]);
140/// let factors = full_piv_lu_matrix(&matrix).unwrap();
141/// assert_eq!(factors.u.nrows(), 2);
142/// assert_eq!(factors.u.ncols(), 2);
143/// ```
144#[derive(Debug, Clone)]
145pub struct FullPivLuMatrixResult<T> {
146    /// Left permutation matrix.
147    pub p: Matrix<T>,
148    /// Lower triangular factor.
149    pub l: Matrix<T>,
150    /// Upper triangular factor.
151    pub u: Matrix<T>,
152    /// Right permutation matrix.
153    pub q: Matrix<T>,
154}
155
156/// Scalar bound accepted by tensor4all's typed linalg wrappers.
157pub trait BackendLinalgScalar: TensorScalar {}
158
159/// Scalar types that can solve `T * P = Pi1` via a right full-pivoting LU
160/// solve on the tenferro backend.
161///
162/// This is the foundational seam for matrix cross-interpolation (CI)
163/// materialization: it lets an algorithm layer ask for the backend's
164/// full-pivot LU solve without depending on any higher crate. The four
165/// supported scalar types (f32, f64, Complex32, Complex64) are implemented
166/// here; the backend solves each in its own scalar type along its own kernel
167/// route, and this trait adds no promotion of its own.
168///
169/// # Errors
170///
171/// Returns an error when the pivot matrix is not square, the shapes are
172/// incompatible, or the backend solve fails.
173pub trait FullPivLuScalar: BackendLinalgScalar {
174    /// Solve `T * P = Pi1` for `T`, where `P` is the pivot matrix (column-major).
175    ///
176    /// # Errors
177    ///
178    /// Returns a [`BackendLinalgError`] when the pivot matrix is not square
179    /// (a shape mismatch), when the shapes are incompatible (`lhs_cols !=
180    /// pivot_rows`, a shape mismatch), or when the backend full-pivot LU solve
181    /// fails (a backend failure).
182    fn solve_right_full_piv_lu(
183        lhs_values: &[Self],
184        lhs_rows: usize,
185        lhs_cols: usize,
186        pivot_values: &[Self],
187        pivot_rows: usize,
188        pivot_cols: usize,
189    ) -> std::result::Result<Vec<Self>, BackendLinalgError>;
190
191    /// Same solve, in a caller-owned execution context.
192    ///
193    /// The shapes and values are interpreted exactly as in
194    /// [`Self::solve_right_full_piv_lu`]; only the engine that performs the
195    /// solve is the caller's, so the work runs on the caller's backend instead
196    /// of the process-global one. The context is entered after the operands are
197    /// prepared and validated; a caller that must avoid an entry for a
198    /// degenerate input decides that before calling this method (the
199    /// materialization layer's all-zero pivot shortcut is one such caller).
200    ///
201    /// # Errors
202    ///
203    /// Returns a [`BackendLinalgError`] when the pivot matrix is not square
204    /// (a shape mismatch), when the shapes are incompatible (`lhs_cols !=
205    /// pivot_rows`, a shape mismatch), when the context rejects the entry (a
206    /// nested session on this thread, with the original entry error kept as the
207    /// source), or when the backend full-pivot LU solve fails (a backend
208    /// failure).
209    fn solve_right_full_piv_lu_in(
210        context: &CpuExecutionContext,
211        lhs_values: &[Self],
212        lhs_rows: usize,
213        lhs_cols: usize,
214        pivot_values: &[Self],
215        pivot_rows: usize,
216        pivot_cols: usize,
217    ) -> std::result::Result<Vec<Self>, BackendLinalgError>;
218
219    /// Same solve, on an engine the caller already holds.
220    ///
221    /// Neither this method nor the backend enters a context, and nothing
222    /// consults the process-global one. The shapes and values are interpreted
223    /// exactly as in [`Self::solve_right_full_piv_lu`], and they are validated
224    /// before the engine is used.
225    ///
226    /// # Errors
227    ///
228    /// Returns a [`BackendLinalgError`] when the pivot matrix is not square
229    /// (a shape mismatch), when the shapes are incompatible (`lhs_cols !=
230    /// pivot_rows`, a shape mismatch), or when the backend full-pivot LU solve
231    /// fails on the supplied engine (a backend failure).
232    fn solve_right_full_piv_lu_on_session(
233        session: &mut dyn tenferro_tensor::Engine,
234        lhs_values: &[Self],
235        lhs_rows: usize,
236        lhs_cols: usize,
237        pivot_values: &[Self],
238        pivot_rows: usize,
239        pivot_cols: usize,
240    ) -> std::result::Result<Vec<Self>, BackendLinalgError>;
241}
242
243macro_rules! impl_full_piv_lu_scalar {
244    ($t:ty) => {
245        impl FullPivLuScalar for $t {
246            fn solve_right_full_piv_lu(
247                lhs_values: &[Self],
248                lhs_rows: usize,
249                lhs_cols: usize,
250                pivot_values: &[Self],
251                pivot_rows: usize,
252                pivot_cols: usize,
253            ) -> std::result::Result<Vec<Self>, BackendLinalgError> {
254                let inputs = FullPivLuSolveInputs::new(
255                    lhs_values,
256                    lhs_rows,
257                    lhs_cols,
258                    pivot_values,
259                    pivot_rows,
260                    pivot_cols,
261                )?;
262                let solved_t =
263                    with_default_session(|session| inputs.solve(session)).map_err(|e| {
264                        BackendLinalgError::from(anyhow::anyhow!("full_piv_lu_solve failed: {e}"))
265                    })?;
266                inputs.finish::<Self>(solved_t)
267            }
268
269            fn solve_right_full_piv_lu_in(
270                context: &CpuExecutionContext,
271                lhs_values: &[Self],
272                lhs_rows: usize,
273                lhs_cols: usize,
274                pivot_values: &[Self],
275                pivot_rows: usize,
276                pivot_cols: usize,
277            ) -> std::result::Result<Vec<Self>, BackendLinalgError> {
278                let inputs = FullPivLuSolveInputs::new(
279                    lhs_values,
280                    lhs_rows,
281                    lhs_cols,
282                    pivot_values,
283                    pivot_rows,
284                    pivot_cols,
285                )?;
286                // A rejected entry keeps its own error as the source: the
287                // caller asked for its context, so a worker refusal must stay
288                // visible as a refusal rather than as text. Validation stays
289                // before the entry: a malformed operand must fail without
290                // waiting for the caller's engine.
291                let solved_t = context
292                    .with_concrete_session(|session| inputs.solve(session.engine()))
293                    .map_err(|error| BackendLinalgError::from(anyhow::Error::new(error)))?
294                    .map_err(|error| {
295                        BackendLinalgError::from(anyhow::anyhow!(
296                            "full_piv_lu_solve failed: {error}"
297                        ))
298                    })?;
299                inputs.finish::<Self>(solved_t)
300            }
301
302            fn solve_right_full_piv_lu_on_session(
303                session: &mut dyn tenferro_tensor::Engine,
304                lhs_values: &[Self],
305                lhs_rows: usize,
306                lhs_cols: usize,
307                pivot_values: &[Self],
308                pivot_rows: usize,
309                pivot_cols: usize,
310            ) -> std::result::Result<Vec<Self>, BackendLinalgError> {
311                let inputs = FullPivLuSolveInputs::new(
312                    lhs_values,
313                    lhs_rows,
314                    lhs_cols,
315                    pivot_values,
316                    pivot_rows,
317                    pivot_cols,
318                )?;
319                let solved_t = inputs.solve(session).map_err(|error| {
320                    BackendLinalgError::from(anyhow::anyhow!("full_piv_lu_solve failed: {error}"))
321                })?;
322                inputs.finish::<Self>(solved_t)
323            }
324        }
325    };
326}
327
328/// Validated, transposed operands for a right full-pivot LU solve.
329///
330/// Preparing them is engine-free on purpose: an invalid shape must fail
331/// before any context is entered.
332struct FullPivLuSolveInputs {
333    pivot_tensor: tenferro_tensor::Tensor,
334    lhs_tensor: tenferro_tensor::Tensor,
335    lhs_cols: usize,
336    lhs_rows: usize,
337}
338
339impl FullPivLuSolveInputs {
340    fn new<T: BackendLinalgScalar + num_traits::Zero>(
341        lhs_values: &[T],
342        lhs_rows: usize,
343        lhs_cols: usize,
344        pivot_values: &[T],
345        pivot_rows: usize,
346        pivot_cols: usize,
347    ) -> std::result::Result<Self, BackendLinalgError> {
348        if pivot_rows != pivot_cols {
349            return Err(BackendLinalgError::from(anyhow::anyhow!(
350                "full-pivot solve requires a square pivot matrix, got {}x{}",
351                pivot_rows,
352                pivot_cols
353            )));
354        }
355        if lhs_cols != pivot_rows {
356            return Err(BackendLinalgError::from(anyhow::anyhow!(
357                "cannot solve T * P = Pi1 with Pi1 shape {}x{} and P shape {}x{}",
358                lhs_rows,
359                lhs_cols,
360                pivot_rows,
361                pivot_cols
362            )));
363        }
364
365        let lhs_t = transpose_column_major(lhs_values, lhs_rows, lhs_cols);
366        let pivot_t = transpose_column_major(pivot_values, pivot_rows, pivot_cols);
367        let pivot_tensor =
368            tenferro_tensor::Tensor::from_vec_col_major(vec![pivot_cols, pivot_rows], pivot_t)
369                .map_err(|e| BackendLinalgError::from(anyhow::Error::new(e)))?;
370        let lhs_tensor =
371            tenferro_tensor::Tensor::from_vec_col_major(vec![lhs_cols, lhs_rows], lhs_t)
372                .map_err(|e| BackendLinalgError::from(anyhow::Error::new(e)))?;
373        Ok(Self {
374            pivot_tensor,
375            lhs_tensor,
376            lhs_cols,
377            lhs_rows,
378        })
379    }
380
381    fn solve(
382        &self,
383        session: &mut dyn tenferro_tensor::Engine,
384    ) -> tenferro_tensor::Result<tenferro_tensor::Tensor> {
385        self.pivot_tensor
386            .full_piv_lu_solve(&self.lhs_tensor, session)
387    }
388
389    fn finish<T: BackendLinalgScalar + num_traits::Zero>(
390        &self,
391        solved_t: tenferro_tensor::Tensor,
392    ) -> std::result::Result<Vec<T>, BackendLinalgError> {
393        let solved_t_values = solved_t.as_slice::<T>().map_err(|e| {
394            BackendLinalgError::from(anyhow::anyhow!(
395                "full_piv_lu_solve returned unexpected dtype: {e}"
396            ))
397        })?;
398        Ok(transpose_column_major(
399            solved_t_values,
400            self.lhs_cols,
401            self.lhs_rows,
402        ))
403    }
404}
405
406impl_full_piv_lu_scalar!(f32);
407impl_full_piv_lu_scalar!(f64);
408impl_full_piv_lu_scalar!(num_complex::Complex32);
409impl_full_piv_lu_scalar!(num_complex::Complex64);
410
411/// Transpose a column-major flat buffer.
412fn transpose_column_major<T: Copy + num_traits::Zero>(
413    values: &[T],
414    nrows: usize,
415    ncols: usize,
416) -> Vec<T> {
417    let mut out = vec![T::zero(); nrows * ncols];
418    for col in 0..ncols {
419        for row in 0..nrows {
420            out[col + ncols * row] = values[row + nrows * col];
421        }
422    }
423    out
424}
425
426impl<T: TensorScalar> BackendLinalgScalar for T {}
427
428/// Scalar types supported by [`solve_matrix`].
429/// `f64` and `Complex64` are solved directly. `f32` and `Complex32` are
430/// promoted to the corresponding 64-bit dtype for the backend solve and then
431/// converted back, because the current tenferro CPU LU solve is double
432/// precision only.
433/// # Examples
434/// ```
435/// use tensor4all_tensorbackend::{from_vec2d, solve_matrix};
436/// let a = from_vec2d(vec![vec![2.0_f32, 1.0], vec![1.0, 2.0]]);
437/// let b = from_vec2d(vec![vec![1.0_f32], vec![0.0]]);
438/// let x = solve_matrix(&a, &b).unwrap();
439/// assert!((x[[0, 0]] - 2.0 / 3.0).abs() < 1.0e-6);
440/// ```
441pub trait MatrixSolveScalar: BackendLinalgScalar + crate::matrix::MatrixScalar {
442    #[doc(hidden)]
443    fn solve_matrix_impl(a: &Matrix<Self>, b: &Matrix<Self>) -> Result<Matrix<Self>>;
444
445    #[doc(hidden)]
446    fn solve_matrix_owned_impl(a: Matrix<Self>, b: Matrix<Self>) -> Result<Matrix<Self>> {
447        Self::solve_matrix_impl(&a, &b)
448    }
449
450    /// Solve `a * x = b` on an engine the caller already holds.
451    ///
452    /// The engine-taking counterpart of [`Self::solve_matrix_impl`], for
453    /// callers that own their execution domain rather than a context they may
454    /// enter themselves.
455    ///
456    /// # Errors
457    ///
458    /// Returns the backend's solve failure: a non-square factor, an
459    /// incompatible right-hand side, a singular factor, or an engine-level
460    /// execution failure.
461    #[doc(hidden)]
462    fn solve_matrix_in_impl(
463        session: &mut dyn tenferro_tensor::Engine,
464        a: &Matrix<Self>,
465        b: &Matrix<Self>,
466    ) -> Result<Matrix<Self>>;
467}
468
469/// Error returned by the CPU backend linear-algebra dispatch helpers.
470///
471/// Wraps the underlying tenferro or shape diagnostic, preserving its source
472/// chain.
473///
474/// # Remedies
475/// - Shape/dtype mismatch: verify matrix dims and scalar dtypes against the
476///   operation contract before calling.
477/// - Singular or invalid input: check matrix conditioning and finite values
478///   before solve/factorization operations.
479/// - Backend failure: the wrapped source chain identifies the failing stage.
480#[derive(Debug, thiserror::Error)]
481#[error("backend linear algebra failed: {source}")]
482pub struct BackendLinalgError {
483    /// Original backend or shape diagnostic.
484    #[source]
485    pub source: anyhow::Error,
486}
487
488impl From<anyhow::Error> for BackendLinalgError {
489    fn from(source: anyhow::Error) -> Self {
490        Self { source }
491    }
492}
493
494/// Scalar types supported by [`triangular_solve_matrix`].
495/// `f64` and `Complex64` are solved directly. `f32` and `Complex32` are
496/// promoted to the corresponding 64-bit dtype for the backend solve and then
497/// converted back, because the current tenferro CPU triangular solve is double
498/// precision only.
499/// # Examples
500/// ```
501/// use tensor4all_tensorbackend::{from_vec2d, triangular_solve_matrix};
502/// let a = from_vec2d(vec![vec![2.0_f64, 0.0], vec![1.0, 3.0]]);
503/// let b = from_vec2d(vec![vec![2.0_f64], vec![7.0]]);
504/// let x = triangular_solve_matrix(&a, &b, true, true, false, false).unwrap();
505/// assert!((x[[0, 0]] - 1.0).abs() < 1.0e-12);
506/// assert!((x[[1, 0]] - 2.0).abs() < 1.0e-12);
507/// ```
508pub trait MatrixTriangularSolveScalar: BackendLinalgScalar + crate::matrix::MatrixScalar {
509    #[doc(hidden)]
510    fn triangular_solve_matrix_impl(
511        a: &Matrix<Self>,
512        b: &Matrix<Self>,
513        left_side: bool,
514        lower: bool,
515        transpose_a: bool,
516        unit_diagonal: bool,
517    ) -> Result<Matrix<Self>>;
518
519    #[doc(hidden)]
520    fn triangular_solve_matrix_owned_impl(
521        a: Matrix<Self>,
522        b: Matrix<Self>,
523        left_side: bool,
524        lower: bool,
525        transpose_a: bool,
526        unit_diagonal: bool,
527    ) -> Result<Matrix<Self>> {
528        Self::triangular_solve_matrix_impl(&a, &b, left_side, lower, transpose_a, unit_diagonal)
529    }
530}
531
532/// Small-matrix diagnostics produced by the successive randomized compression
533/// stopping estimator.
534///
535/// `error` is the Appendix C randomized residual estimate and `norm` is the
536/// corresponding Frobenius norm estimate. Both values use the sketch width as
537/// their normalization factor.
538///
539/// # Examples
540///
541/// ```
542/// use tensor4all_tensorbackend::{src_error_estimate, Matrix};
543///
544/// let r = Matrix::from_col_major_vec(1, 1, vec![2.0_f64]);
545/// let estimate = src_error_estimate(&r).unwrap();
546/// assert!((estimate.error - 2.0).abs() < 1.0e-12);
547/// assert!((estimate.norm - 2.0).abs() < 1.0e-12);
548/// ```
549#[derive(Debug, Clone, Copy, PartialEq)]
550pub struct SrcErrorEstimate {
551    /// Estimated residual magnitude from the inverse-adjoint QR factor.
552    pub error: f64,
553    /// Estimated norm of the sketched tensor from the QR factor.
554    pub norm: f64,
555}
556
557/// Compute the Appendix C SRC error and norm estimates from an upper-triangular
558/// QR factor `R`.
559///
560/// Provenance: the formulas are Eq. (err-est) and Eq. (norm-est) in Appendix C
561/// of Camaño--Epperly--Tropp, [arXiv:2504.06475](https://arxiv.org/abs/2504.06475),
562/// cross-checked against `chriscamano/RandomMPOMPS/code/tensornetwork/incrementalqr.cpp::get_error_estimate`
563/// (lines 106--119). The use of actual `R` plus an `R†` solve is an equivalent
564/// representation derived in the audit; it is not a literal port of the
565/// author's inverse-`R` storage.
566///
567/// The helper explicitly builds `R†` before solving `R† G = I`, so complex
568/// inputs use the Hermitian adjoint rather than a plain transpose. The solve is
569/// delegated to the configured tensor4all backend and is restricted to the
570/// small sketch matrix; no general dense inverse routine is used.
571///
572/// # Errors
573///
574/// Returns [`BackendLinalgError`] when `r` is empty, non-square,
575/// non-triangular, singular, or contains non-finite values, or when the backend
576/// triangular solve fails.
577///
578/// # Examples
579///
580/// ```
581/// use tensor4all_tensorbackend::{src_error_estimate, Matrix};
582///
583/// let r = Matrix::from_col_major_vec(2, 2, vec![2.0_f64, 0.0, 1.0, 3.0]);
584/// let estimate = src_error_estimate(&r).unwrap();
585/// assert!(estimate.error.is_finite());
586/// assert!(estimate.norm.is_finite());
587/// ```
588pub fn src_error_estimate<T>(
589    r: &Matrix<T>,
590) -> std::result::Result<SrcErrorEstimate, BackendLinalgError>
591where
592    T: MatrixTriangularSolveScalar + ComplexFloat,
593{
594    let inverse_adjoint = src_inverse_adjoint(r)?;
595    src_error_estimate_from_inverse_adjoint(r, &inverse_adjoint)
596}
597
598/// Compute the Appendix C SRC estimates from a general square sketch factor.
599///
600/// This variant is equivalent to [`src_error_estimate`] but uses a general
601/// solve for factors whose columns have been restored from pivoted QR order.
602/// The matrix is a small SRC sketch factor, not a full tensor materialization.
603///
604/// # Errors
605///
606/// Returns [`BackendLinalgError`] when `factor` is empty, non-square, singular,
607/// contains non-finite values, or when the configured general solve fails.
608///
609/// # Examples
610///
611/// ```
612/// use tensor4all_tensorbackend::{src_error_estimate_general, Matrix};
613///
614/// let factor = Matrix::from_col_major_vec(2, 2, vec![0.0_f64, 3.0, 2.0, 1.0]);
615/// let estimate = src_error_estimate_general(&factor).unwrap();
616/// assert!(estimate.error.is_finite());
617/// assert!((estimate.norm - (14.0_f64 / 2.0).sqrt()).abs() < 1.0e-12);
618/// ```
619pub fn src_error_estimate_general<T>(
620    factor: &Matrix<T>,
621) -> std::result::Result<SrcErrorEstimate, BackendLinalgError>
622where
623    T: MatrixSolveScalar + ComplexFloat,
624{
625    let nrows = factor.nrows();
626    let ncols = factor.ncols();
627    if nrows != ncols {
628        return Err(BackendLinalgError::from(anyhow!(
629            "SRC estimator requires a square factor, got {nrows}x{ncols}"
630        )));
631    }
632    if nrows == 0 {
633        return Err(BackendLinalgError::from(anyhow!(
634            "SRC estimator requires a non-empty factor"
635        )));
636    }
637    if factor
638        .as_col_major_slice()
639        .iter()
640        .any(|value| !value.matrix_abs_sq().is_finite())
641    {
642        return Err(BackendLinalgError::from(anyhow!(
643            "SRC estimator requires finite entries in its factor"
644        )));
645    }
646    let mut adjoint = Matrix::zeros(nrows, ncols);
647    let mut identity = Matrix::zeros(nrows, ncols);
648    for col in 0..ncols {
649        for row in 0..nrows {
650            adjoint[[row, col]] = factor[[col, row]].conj();
651        }
652        identity[[col, col]] = T::one();
653    }
654    let inverse_adjoint = solve_matrix(&adjoint, &identity).map_err(|error| {
655        BackendLinalgError::from(anyhow!("SRC inverse-adjoint general solve failed: {error}"))
656    })?;
657    src_error_estimate_from_inverse_adjoint(factor, &inverse_adjoint)
658}
659
660/// Compute the inverse adjoint `R^{-†}` used by the Appendix C estimator.
661///
662/// This is crate-visible so incremental QR can initialize the stored
663/// estimator state once and then update it with the block formula from
664/// Appendix C.3 instead of solving the same triangular system after every
665/// appended sketch block.
666pub(crate) fn src_inverse_adjoint<T>(
667    r: &Matrix<T>,
668) -> std::result::Result<Matrix<T>, BackendLinalgError>
669where
670    T: MatrixTriangularSolveScalar + ComplexFloat,
671{
672    let nrows = r.nrows();
673    let ncols = r.ncols();
674    if nrows != ncols {
675        return Err(BackendLinalgError::from(anyhow!(
676            "SRC estimator requires a square R, got {nrows}x{ncols}"
677        )));
678    }
679    if nrows == 0 {
680        return Err(BackendLinalgError::from(anyhow!(
681            "SRC estimator requires a non-empty R"
682        )));
683    }
684
685    // Only the diagonal is checked here; the full-R Frobenius-norm
686    // finiteness check lives solely in `src_error_estimate_from_inverse_adjoint`,
687    // which every `src_error_estimate` call already runs immediately after
688    // this function returns. Duplicating that O(rank^2) accumulation here
689    // would recompute the identical sum for nothing on SRC's adaptive
690    // stopping-test hot path.
691    for col in 0..ncols {
692        let diagonal_sq = r[[col, col]].matrix_abs_sq();
693        if !diagonal_sq.is_finite() || diagonal_sq == 0.0 {
694            return Err(BackendLinalgError::from(anyhow!(
695                "SRC estimator requires a finite, nonzero diagonal in R at ({col}, {col})"
696            )));
697        }
698        for row in col + 1..nrows {
699            if r[[row, col]].matrix_abs_sq() != 0.0 {
700                return Err(BackendLinalgError::from(anyhow!(
701                    "SRC triangular estimator requires upper-triangular R; entry ({row}, {col}) is nonzero"
702                )));
703            }
704        }
705    }
706
707    let mut adjoint = Matrix::zeros(nrows, ncols);
708    for col in 0..ncols {
709        for row in 0..nrows {
710            adjoint[[row, col]] = r[[col, row]].conj();
711        }
712    }
713    let mut identity = Matrix::zeros(nrows, ncols);
714    for diagonal in 0..nrows {
715        identity[[diagonal, diagonal]] = T::one();
716    }
717
718    let inverse_adjoint = triangular_solve_matrix(&adjoint, &identity, true, true, false, false)
719        .map_err(|error| {
720            BackendLinalgError::from(anyhow!(
721                "SRC inverse-adjoint triangular solve failed: {error}"
722            ))
723        })?;
724    Ok(inverse_adjoint)
725}
726
727/// Evaluate the Appendix C estimator from a previously computed `R^{-†}`.
728///
729/// The inverse-adjoint argument is intentionally separate from
730/// [`src_error_estimate`] so incremental QR can reuse its updated triangular
731/// solve state. This helper performs only norm accumulation and validation.
732pub(crate) fn src_error_estimate_from_inverse_adjoint<T>(
733    r: &Matrix<T>,
734    inverse_adjoint: &Matrix<T>,
735) -> std::result::Result<SrcErrorEstimate, BackendLinalgError>
736where
737    T: crate::matrix::MatrixScalar + ComplexFloat,
738{
739    let nrows = r.nrows();
740    let ncols = r.ncols();
741    if nrows != ncols || inverse_adjoint.nrows() != nrows || inverse_adjoint.ncols() != ncols {
742        return Err(BackendLinalgError::from(anyhow!(
743            "SRC estimator requires matching square R and inverse-adjoint factors"
744        )));
745    }
746    if nrows == 0 {
747        return Err(BackendLinalgError::from(anyhow!(
748            "SRC estimator requires a non-empty R"
749        )));
750    }
751
752    let mut norm_sq = 0.0_f64;
753    for value in r.as_col_major_slice() {
754        norm_sq += value.matrix_abs_sq();
755    }
756    if !norm_sq.is_finite() {
757        return Err(BackendLinalgError::from(anyhow!(
758            "SRC estimator requires finite entries in R"
759        )));
760    }
761    let mut inverse_column_error_sq = 0.0_f64;
762    for col in 0..ncols {
763        let column_norm_sq = (0..nrows)
764            .map(|row| inverse_adjoint[[row, col]].matrix_abs_sq())
765            .sum::<f64>();
766        if !column_norm_sq.is_finite() || column_norm_sq == 0.0 {
767            return Err(BackendLinalgError::from(anyhow!(
768                "SRC inverse-adjoint solve returned an invalid column norm at column {col}"
769            )));
770        }
771        inverse_column_error_sq += 1.0 / column_norm_sq;
772    }
773
774    let sketch_width = ncols as f64;
775    let error_sq = inverse_column_error_sq / sketch_width;
776    let norm_estimate_sq = norm_sq / sketch_width;
777    if !error_sq.is_finite() || !norm_estimate_sq.is_finite() {
778        return Err(BackendLinalgError::from(anyhow!(
779            "SRC estimator produced a non-finite estimate"
780        )));
781    }
782    Ok(SrcErrorEstimate {
783        error: error_sq.sqrt(),
784        norm: norm_estimate_sq.sqrt(),
785    })
786}
787
788fn solve_matrix_direct<T>(a: &Matrix<T>, b: &Matrix<T>) -> Result<Matrix<T>>
789where
790    T: BackendLinalgScalar + Copy,
791    Tensor: From<TypedTensor<T>>,
792{
793    with_default_session(|session| Ok(solve_matrix_direct_in(session, a, b)))
794        .map_err(|e| anyhow!("linear solve failed via tenferro-tensor: {e}"))?
795}
796
797fn solve_matrix_direct_in<T>(
798    session: &mut dyn tenferro_tensor::Engine,
799    a: &Matrix<T>,
800    b: &Matrix<T>,
801) -> Result<Matrix<T>>
802where
803    T: BackendLinalgScalar + Copy,
804    Tensor: From<TypedTensor<T>>,
805{
806    solve_matrix_direct_owned_in(session, a.clone(), b.clone())
807}
808
809fn solve_matrix_direct_owned<T>(a: Matrix<T>, b: Matrix<T>) -> Result<Matrix<T>>
810where
811    T: BackendLinalgScalar + Copy,
812    Tensor: From<TypedTensor<T>>,
813{
814    with_default_session(|session| Ok(solve_matrix_direct_owned_in(session, a, b)))
815        .map_err(|e| anyhow!("linear solve failed via tenferro-tensor: {e}"))?
816}
817
818fn solve_matrix_direct_owned_in<T>(
819    session: &mut dyn tenferro_tensor::Engine,
820    a: Matrix<T>,
821    b: Matrix<T>,
822) -> Result<Matrix<T>>
823where
824    T: BackendLinalgScalar + Copy,
825    Tensor: From<TypedTensor<T>>,
826{
827    use tenferro_linalg::TensorLinalgExt;
828
829    let a_tensor: Tensor = a.into_typed_tensor().into();
830    let b_tensor: Tensor = b.into_typed_tensor().into();
831    let result = a_tensor
832        .solve(&b_tensor, session)
833        .map_err(|e| anyhow!("linear solve failed via tenferro-tensor: {e}"))?;
834    let x = try_into_typed_result::<T>("solve", result)?;
835    typed_tensor_to_matrix("solve", x)
836}
837
838/// Solve `A X = B` on a caller-held engine.
839///
840/// Same kernel, conversion and validation as [`solve_matrix`], entered through the
841/// given session instead of the process-global one.
842///
843/// # Errors
844///
845/// Returns [`BackendLinalgError`] under the same conditions as [`solve_matrix`].
846pub fn solve_matrix_in<T>(
847    session: &mut dyn tenferro_tensor::Engine,
848    a: &Matrix<T>,
849    b: &Matrix<T>,
850) -> std::result::Result<Matrix<T>, BackendLinalgError>
851where
852    T: BackendLinalgScalar + Copy,
853    Tensor: From<TypedTensor<T>>,
854{
855    solve_matrix_direct_in(session, a, b).map_err(BackendLinalgError::from)
856}
857
858fn triangular_solve_matrix_direct<T>(
859    a: &Matrix<T>,
860    b: &Matrix<T>,
861    left_side: bool,
862    lower: bool,
863    transpose_a: bool,
864    unit_diagonal: bool,
865) -> Result<Matrix<T>>
866where
867    T: BackendLinalgScalar + Copy,
868    Tensor: From<TypedTensor<T>>,
869{
870    with_default_session(|session| {
871        Ok(triangular_solve_matrix_direct_in(
872            session,
873            a,
874            b,
875            left_side,
876            lower,
877            transpose_a,
878            unit_diagonal,
879        ))
880    })
881    .map_err(|e| anyhow!("triangular solve failed via tenferro-tensor: {e}"))?
882}
883
884#[allow(clippy::too_many_arguments)]
885fn triangular_solve_matrix_direct_in<T>(
886    session: &mut dyn tenferro_tensor::Engine,
887    a: &Matrix<T>,
888    b: &Matrix<T>,
889    left_side: bool,
890    lower: bool,
891    transpose_a: bool,
892    unit_diagonal: bool,
893) -> Result<Matrix<T>>
894where
895    T: BackendLinalgScalar + Copy,
896    Tensor: From<TypedTensor<T>>,
897{
898    triangular_solve_matrix_direct_owned_in(
899        session,
900        a.clone(),
901        b.clone(),
902        left_side,
903        lower,
904        transpose_a,
905        unit_diagonal,
906    )
907}
908
909fn triangular_solve_matrix_direct_owned<T>(
910    a: Matrix<T>,
911    b: Matrix<T>,
912    left_side: bool,
913    lower: bool,
914    transpose_a: bool,
915    unit_diagonal: bool,
916) -> Result<Matrix<T>>
917where
918    T: BackendLinalgScalar + Copy,
919    Tensor: From<TypedTensor<T>>,
920{
921    with_default_session(|session| {
922        Ok(triangular_solve_matrix_direct_owned_in(
923            session,
924            a,
925            b,
926            left_side,
927            lower,
928            transpose_a,
929            unit_diagonal,
930        ))
931    })
932    .map_err(|e| anyhow!("triangular solve failed via tenferro-tensor: {e}"))?
933}
934
935#[allow(clippy::too_many_arguments)]
936fn triangular_solve_matrix_direct_owned_in<T>(
937    session: &mut dyn tenferro_tensor::Engine,
938    a: Matrix<T>,
939    b: Matrix<T>,
940    left_side: bool,
941    lower: bool,
942    transpose_a: bool,
943    unit_diagonal: bool,
944) -> Result<Matrix<T>>
945where
946    T: BackendLinalgScalar + Copy,
947    Tensor: From<TypedTensor<T>>,
948{
949    use tenferro_linalg::TensorLinalgExt;
950
951    let a_tensor: Tensor = a.into_typed_tensor().into();
952    let b_tensor: Tensor = b.into_typed_tensor().into();
953    let result = a_tensor
954        .triangular_solve(
955            &b_tensor,
956            left_side,
957            lower,
958            transpose_a,
959            unit_diagonal,
960            session,
961        )
962        .map_err(|e| anyhow!("triangular solve failed via tenferro-tensor: {e}"))?;
963    let x = try_into_typed_result::<T>("triangular_solve", result)?;
964    typed_tensor_to_matrix("triangular_solve", x)
965}
966
967/// Solve a triangular system on a caller-held engine.
968///
969/// Same kernel, conversion and validation as [`triangular_solve_matrix`], entered
970/// through the given engine instead of the process-global context.
971///
972/// # Errors
973///
974/// Returns [`BackendLinalgError`] under the same conditions as
975/// [`triangular_solve_matrix`].
976#[allow(clippy::too_many_arguments)]
977pub fn triangular_solve_matrix_in<T>(
978    session: &mut dyn tenferro_tensor::Engine,
979    a: &Matrix<T>,
980    b: &Matrix<T>,
981    left_side: bool,
982    lower: bool,
983    transpose_a: bool,
984    unit_diagonal: bool,
985) -> std::result::Result<Matrix<T>, BackendLinalgError>
986where
987    T: BackendLinalgScalar + Copy,
988    Tensor: From<TypedTensor<T>>,
989{
990    triangular_solve_matrix_direct_in(session, a, b, left_side, lower, transpose_a, unit_diagonal)
991        .map_err(BackendLinalgError::from)
992}
993
994impl MatrixSolveScalar for f64 {
995    fn solve_matrix_impl(a: &Matrix<Self>, b: &Matrix<Self>) -> Result<Matrix<Self>> {
996        solve_matrix_direct(a, b)
997    }
998
999    fn solve_matrix_in_impl(
1000        session: &mut dyn tenferro_tensor::Engine,
1001        a: &Matrix<Self>,
1002        b: &Matrix<Self>,
1003    ) -> Result<Matrix<Self>> {
1004        solve_matrix_direct_in(session, a, b)
1005    }
1006
1007    fn solve_matrix_owned_impl(a: Matrix<Self>, b: Matrix<Self>) -> Result<Matrix<Self>> {
1008        solve_matrix_direct_owned(a, b)
1009    }
1010}
1011
1012impl MatrixTriangularSolveScalar for f64 {
1013    fn triangular_solve_matrix_impl(
1014        a: &Matrix<Self>,
1015        b: &Matrix<Self>,
1016        left_side: bool,
1017        lower: bool,
1018        transpose_a: bool,
1019        unit_diagonal: bool,
1020    ) -> Result<Matrix<Self>> {
1021        triangular_solve_matrix_direct(a, b, left_side, lower, transpose_a, unit_diagonal)
1022    }
1023
1024    fn triangular_solve_matrix_owned_impl(
1025        a: Matrix<Self>,
1026        b: Matrix<Self>,
1027        left_side: bool,
1028        lower: bool,
1029        transpose_a: bool,
1030        unit_diagonal: bool,
1031    ) -> Result<Matrix<Self>> {
1032        triangular_solve_matrix_direct_owned(a, b, left_side, lower, transpose_a, unit_diagonal)
1033    }
1034}
1035
1036impl MatrixSolveScalar for Complex64 {
1037    fn solve_matrix_impl(a: &Matrix<Self>, b: &Matrix<Self>) -> Result<Matrix<Self>> {
1038        solve_matrix_direct(a, b)
1039    }
1040
1041    fn solve_matrix_in_impl(
1042        session: &mut dyn tenferro_tensor::Engine,
1043        a: &Matrix<Self>,
1044        b: &Matrix<Self>,
1045    ) -> Result<Matrix<Self>> {
1046        solve_matrix_direct_in(session, a, b)
1047    }
1048
1049    fn solve_matrix_owned_impl(a: Matrix<Self>, b: Matrix<Self>) -> Result<Matrix<Self>> {
1050        solve_matrix_direct_owned(a, b)
1051    }
1052}
1053
1054impl MatrixTriangularSolveScalar for Complex64 {
1055    fn triangular_solve_matrix_impl(
1056        a: &Matrix<Self>,
1057        b: &Matrix<Self>,
1058        left_side: bool,
1059        lower: bool,
1060        transpose_a: bool,
1061        unit_diagonal: bool,
1062    ) -> Result<Matrix<Self>> {
1063        triangular_solve_matrix_direct(a, b, left_side, lower, transpose_a, unit_diagonal)
1064    }
1065
1066    fn triangular_solve_matrix_owned_impl(
1067        a: Matrix<Self>,
1068        b: Matrix<Self>,
1069        left_side: bool,
1070        lower: bool,
1071        transpose_a: bool,
1072        unit_diagonal: bool,
1073    ) -> Result<Matrix<Self>> {
1074        triangular_solve_matrix_direct_owned(a, b, left_side, lower, transpose_a, unit_diagonal)
1075    }
1076}
1077
1078impl MatrixSolveScalar for f32 {
1079    fn solve_matrix_impl(a: &Matrix<Self>, b: &Matrix<Self>) -> Result<Matrix<Self>> {
1080        let a64 = Matrix::from_col_major_vec(
1081            a.nrows(),
1082            a.ncols(),
1083            a.as_col_major_slice()
1084                .iter()
1085                .map(|&value| value as f64)
1086                .collect(),
1087        );
1088        let b64 = Matrix::from_col_major_vec(
1089            b.nrows(),
1090            b.ncols(),
1091            b.as_col_major_slice()
1092                .iter()
1093                .map(|&value| value as f64)
1094                .collect(),
1095        );
1096        let x64 = solve_matrix_direct(&a64, &b64)?;
1097        Ok(Matrix::from_col_major_vec(
1098            x64.nrows(),
1099            x64.ncols(),
1100            x64.as_col_major_slice()
1101                .iter()
1102                .map(|&value| value as f32)
1103                .collect(),
1104        ))
1105    }
1106
1107    fn solve_matrix_in_impl(
1108        session: &mut dyn tenferro_tensor::Engine,
1109        a: &Matrix<Self>,
1110        b: &Matrix<Self>,
1111    ) -> Result<Matrix<Self>> {
1112        // Same narrow-scalar promotion as the global route: solve in double precision on the caller's session and
1113        // narrow the result.
1114        let a64 = Matrix::from_col_major_vec(
1115            a.nrows(),
1116            a.ncols(),
1117            a.as_col_major_slice()
1118                .iter()
1119                .map(|&value| value as f64)
1120                .collect(),
1121        );
1122        let b64 = Matrix::from_col_major_vec(
1123            b.nrows(),
1124            b.ncols(),
1125            b.as_col_major_slice()
1126                .iter()
1127                .map(|&value| value as f64)
1128                .collect(),
1129        );
1130        let x64 = solve_matrix_direct_in(session, &a64, &b64)?;
1131        Ok(Matrix::from_col_major_vec(
1132            x64.nrows(),
1133            x64.ncols(),
1134            x64.as_col_major_slice()
1135                .iter()
1136                .map(|&value| value as f32)
1137                .collect(),
1138        ))
1139    }
1140}
1141
1142impl MatrixTriangularSolveScalar for f32 {
1143    fn triangular_solve_matrix_impl(
1144        a: &Matrix<Self>,
1145        b: &Matrix<Self>,
1146        left_side: bool,
1147        lower: bool,
1148        transpose_a: bool,
1149        unit_diagonal: bool,
1150    ) -> Result<Matrix<Self>> {
1151        let a64 = Matrix::from_col_major_vec(
1152            a.nrows(),
1153            a.ncols(),
1154            a.as_col_major_slice()
1155                .iter()
1156                .map(|&value| value as f64)
1157                .collect(),
1158        );
1159        let b64 = Matrix::from_col_major_vec(
1160            b.nrows(),
1161            b.ncols(),
1162            b.as_col_major_slice()
1163                .iter()
1164                .map(|&value| value as f64)
1165                .collect(),
1166        );
1167        let x64 = triangular_solve_matrix_direct(
1168            &a64,
1169            &b64,
1170            left_side,
1171            lower,
1172            transpose_a,
1173            unit_diagonal,
1174        )?;
1175        Ok(Matrix::from_col_major_vec(
1176            x64.nrows(),
1177            x64.ncols(),
1178            x64.as_col_major_slice()
1179                .iter()
1180                .map(|&value| value as f32)
1181                .collect(),
1182        ))
1183    }
1184}
1185
1186impl MatrixSolveScalar for Complex32 {
1187    fn solve_matrix_impl(a: &Matrix<Self>, b: &Matrix<Self>) -> Result<Matrix<Self>> {
1188        let a64 = Matrix::from_col_major_vec(
1189            a.nrows(),
1190            a.ncols(),
1191            a.as_col_major_slice()
1192                .iter()
1193                .map(|&value| Complex64::new(value.re as f64, value.im as f64))
1194                .collect(),
1195        );
1196        let b64 = Matrix::from_col_major_vec(
1197            b.nrows(),
1198            b.ncols(),
1199            b.as_col_major_slice()
1200                .iter()
1201                .map(|&value| Complex64::new(value.re as f64, value.im as f64))
1202                .collect(),
1203        );
1204        let x64 = solve_matrix_direct(&a64, &b64)?;
1205        Ok(Matrix::from_col_major_vec(
1206            x64.nrows(),
1207            x64.ncols(),
1208            x64.as_col_major_slice()
1209                .iter()
1210                .map(|&value| Complex32::new(value.re as f32, value.im as f32))
1211                .collect(),
1212        ))
1213    }
1214
1215    fn solve_matrix_in_impl(
1216        session: &mut dyn tenferro_tensor::Engine,
1217        a: &Matrix<Self>,
1218        b: &Matrix<Self>,
1219    ) -> Result<Matrix<Self>> {
1220        // Same narrow-scalar promotion as the global route: solve in double precision on the caller's session and
1221        // narrow the result.
1222        let a64 = Matrix::from_col_major_vec(
1223            a.nrows(),
1224            a.ncols(),
1225            a.as_col_major_slice()
1226                .iter()
1227                .map(|&value| Complex64::new(value.re as f64, value.im as f64))
1228                .collect(),
1229        );
1230        let b64 = Matrix::from_col_major_vec(
1231            b.nrows(),
1232            b.ncols(),
1233            b.as_col_major_slice()
1234                .iter()
1235                .map(|&value| Complex64::new(value.re as f64, value.im as f64))
1236                .collect(),
1237        );
1238        let x64 = solve_matrix_direct_in(session, &a64, &b64)?;
1239        Ok(Matrix::from_col_major_vec(
1240            x64.nrows(),
1241            x64.ncols(),
1242            x64.as_col_major_slice()
1243                .iter()
1244                .map(|&value| Complex32::new(value.re as f32, value.im as f32))
1245                .collect(),
1246        ))
1247    }
1248}
1249
1250impl MatrixTriangularSolveScalar for Complex32 {
1251    fn triangular_solve_matrix_impl(
1252        a: &Matrix<Self>,
1253        b: &Matrix<Self>,
1254        left_side: bool,
1255        lower: bool,
1256        transpose_a: bool,
1257        unit_diagonal: bool,
1258    ) -> Result<Matrix<Self>> {
1259        let a64 = Matrix::from_col_major_vec(
1260            a.nrows(),
1261            a.ncols(),
1262            a.as_col_major_slice()
1263                .iter()
1264                .map(|&value| Complex64::new(value.re as f64, value.im as f64))
1265                .collect(),
1266        );
1267        let b64 = Matrix::from_col_major_vec(
1268            b.nrows(),
1269            b.ncols(),
1270            b.as_col_major_slice()
1271                .iter()
1272                .map(|&value| Complex64::new(value.re as f64, value.im as f64))
1273                .collect(),
1274        );
1275        let x64 = triangular_solve_matrix_direct(
1276            &a64,
1277            &b64,
1278            left_side,
1279            lower,
1280            transpose_a,
1281            unit_diagonal,
1282        )?;
1283        Ok(Matrix::from_col_major_vec(
1284            x64.nrows(),
1285            x64.ncols(),
1286            x64.as_col_major_slice()
1287                .iter()
1288                .map(|&value| Complex32::new(value.re as f32, value.im as f32))
1289                .collect(),
1290        ))
1291    }
1292}
1293
1294fn tensor_scalar_dtype<T: TensorScalar>() -> DType {
1295    T::dtype()
1296}
1297
1298fn try_into_typed_result<T: TensorScalar>(
1299    op: &'static str,
1300    tensor: Tensor,
1301) -> Result<TypedTensor<T>> {
1302    let actual = tensor.dtype();
1303    T::into_typed(tensor).map_err(|source| {
1304        anyhow!(
1305            "{op}: dtype mismatch lhs={actual:?} rhs={:?}: {source}",
1306            tensor_scalar_dtype::<T>()
1307        )
1308    })
1309}
1310
1311fn convert_for_typed<T: TensorScalar>(op: &'static str, tensor: Tensor) -> Result<TypedTensor<T>> {
1312    let expected = tensor_scalar_dtype::<T>();
1313    let tensor = if tensor.dtype() == expected {
1314        tensor
1315    } else {
1316        with_default_session(|session| tensor.convert(expected, session))
1317            .map_err(|e| anyhow!("{op}: dtype conversion to {expected:?} failed: {e}"))?
1318    };
1319    try_into_typed_result::<T>(op, tensor)
1320}
1321
1322fn matrix_to_typed_tensor<T>(matrix: &Matrix<T>) -> TypedTensor<T>
1323where
1324    T: TensorScalar + Copy,
1325{
1326    TypedTensor::from_vec_col_major(
1327        vec![matrix.nrows(), matrix.ncols()],
1328        matrix.as_col_major_slice().to_vec(),
1329    )
1330    .unwrap_or_else(|error| {
1331        crate::invariant_violated("validated matrix rejected by tenferro", error)
1332    })
1333}
1334
1335fn typed_tensor_to_matrix<T>(op: &'static str, tensor: TypedTensor<T>) -> Result<Matrix<T>>
1336where
1337    T: TensorScalar + Copy,
1338{
1339    Matrix::try_from_typed_tensor(tensor).map_err(|err| anyhow!("{op}: {err}"))
1340}
1341
1342fn require_host_linalg_tensor<T: TensorScalar>(
1343    op: &'static str,
1344    tensor: TypedTensor<T>,
1345) -> Result<TypedTensor<T>> {
1346    tensor
1347        .host_data()
1348        .map_err(|error| anyhow!("{op}: result must be host-backed: {error}"))?;
1349    Ok(tensor)
1350}
1351
1352/// Compute a thin/economy SVD on a typed tensor.
1353/// # Errors
1354///
1355/// Returns a [`BackendLinalgError`] when `a` is not host-backed, when the
1356/// context rejects the entry, or when the backend SVD fails.
1357///
1358pub fn svd_backend<T>(a: &TypedTensor<T>) -> std::result::Result<SvdResult<T>, BackendLinalgError>
1359where
1360    T: BackendLinalgScalar,
1361{
1362    let tensor = T::into_tensor(
1363        a.shape().to_vec(),
1364        a.host_data()
1365            .map_err(|e| anyhow!("SVD input host access failed: {e}"))?
1366            .to_vec(),
1367    )
1368    .map_err(|e| anyhow!("SVD input tensor construction failed: {e}"))?;
1369    with_default_session(|session| Ok(svd_backend_in(session, tensor)))
1370        .map_err(|e| anyhow!("SVD computation failed via tenferro-tensor: {e}"))?
1371}
1372
1373/// SVD of a typed tensor on a caller-held engine.
1374///
1375/// # Errors
1376///
1377/// Returns [`BackendLinalgError`] under the same conditions as [`svd_backend`].
1378pub fn svd_backend_in<T>(
1379    session: &mut dyn tenferro_tensor::Engine,
1380    tensor: Tensor,
1381) -> std::result::Result<SvdResult<T>, BackendLinalgError>
1382where
1383    T: BackendLinalgScalar,
1384{
1385    use tenferro_linalg::TensorLinalgExt;
1386
1387    let (u, s, vt) = tensor
1388        .svd(session)
1389        .map_err(|e| anyhow!("SVD computation failed via tenferro-tensor: {e}"))?;
1390    Ok(SvdResult {
1391        u: require_host_linalg_tensor("svd", convert_for_typed::<T>("svd", u)?)?,
1392        s: require_host_linalg_tensor("svd", convert_for_typed::<T::Real>("svd", s)?)?,
1393        vt: require_host_linalg_tensor("svd", convert_for_typed::<T>("svd", vt)?)?,
1394    })
1395}
1396
1397/// Compute a thin/economy QR decomposition, consuming the input tensor so its
1398/// column-major storage can be transferred to the backend without copying.
1399/// # Errors
1400///
1401/// Returns a [`BackendLinalgError`] when `a` is not host-backed, when the
1402/// context rejects the entry, or when the backend QR fails.
1403///
1404pub fn qr_backend<T>(
1405    a: TypedTensor<T>,
1406) -> std::result::Result<(TypedTensor<T>, TypedTensor<T>), BackendLinalgError>
1407where
1408    T: BackendLinalgScalar,
1409{
1410    let (shape, data) = a
1411        .into_vec_col_major()
1412        .map_err(|e| anyhow!("QR input host access failed: {e}"))?;
1413    let tensor = T::into_tensor(shape, data)
1414        .map_err(|e| anyhow!("QR input tensor construction failed: {e}"))?;
1415    with_default_session(|session| Ok(qr_backend_in(session, tensor)))
1416        .map_err(|e| anyhow!("QR computation failed via tenferro-tensor: {e}"))?
1417}
1418
1419/// QR of a typed tensor on a caller-held engine.
1420///
1421/// # Errors
1422///
1423/// Returns [`BackendLinalgError`] under the same conditions as [`qr_backend`].
1424pub fn qr_backend_in<T>(
1425    session: &mut dyn tenferro_tensor::Engine,
1426    tensor: Tensor,
1427) -> std::result::Result<(TypedTensor<T>, TypedTensor<T>), BackendLinalgError>
1428where
1429    T: BackendLinalgScalar,
1430{
1431    use tenferro_linalg::TensorLinalgExt;
1432
1433    let (q, r) = tensor
1434        .qr(session)
1435        .map_err(|e| anyhow!("QR computation failed via tenferro-tensor: {e}"))?;
1436    Ok((
1437        convert_for_typed::<T>("qr", q)?,
1438        convert_for_typed::<T>("qr", r)?,
1439    ))
1440}
1441
1442/// Solve `A X = B` with the configured tenferro backend.
1443/// # Errors
1444/// Returns a [`BackendLinalgError`] when `a` or `b` is not host-backed, when
1445/// the context rejects the entry, or when the solve fails (for example a
1446/// singular `a`).
1447pub fn solve_backend<T>(
1448    a: &TypedTensor<T>,
1449    b: &TypedTensor<T>,
1450) -> std::result::Result<TypedTensor<T>, BackendLinalgError>
1451where
1452    T: BackendLinalgScalar,
1453{
1454    let a_tensor = T::into_tensor(
1455        a.shape().to_vec(),
1456        a.host_data()
1457            .map_err(|e| anyhow!("solve input host access failed: {e}"))?
1458            .to_vec(),
1459    )
1460    .map_err(|e| anyhow!("solve lhs tensor construction failed: {e}"))?;
1461    let b_tensor = T::into_tensor(
1462        b.shape().to_vec(),
1463        b.host_data()
1464            .map_err(|e| anyhow!("solve rhs host access failed: {e}"))?
1465            .to_vec(),
1466    )
1467    .map_err(|e| anyhow!("solve rhs tensor construction failed: {e}"))?;
1468    let result = with_default_session(|session| a_tensor.solve(&b_tensor, session))
1469        .map_err(|e| anyhow!("linear solve failed via tenferro-tensor: {e}"))?;
1470    try_into_typed_result::<T>("solve", result).map_err(BackendLinalgError::from)
1471}
1472
1473/// Solve a triangular system with the configured tenferro backend.
1474/// If `left_side` is true, this solves `op(A) X = B`; otherwise it solves
1475/// `X op(A) = B`. `lower` selects the triangular half, `transpose_a` applies
1476/// a transpose to `A`, and `unit_diagonal` treats the diagonal of `A` as ones.
1477/// # Errors
1478///
1479/// Returns a [`BackendLinalgError`] when `a` or `b` is not host-backed, when
1480/// the context rejects the entry, or when the triangular solve fails
1481/// (for example a singular `a` or inconsistent triangular flags).
1482///
1483pub fn triangular_solve_backend<T>(
1484    a: &TypedTensor<T>,
1485    b: &TypedTensor<T>,
1486    left_side: bool,
1487    lower: bool,
1488    transpose_a: bool,
1489    unit_diagonal: bool,
1490) -> std::result::Result<TypedTensor<T>, BackendLinalgError>
1491where
1492    T: BackendLinalgScalar,
1493{
1494    let a_tensor = T::into_tensor(
1495        a.shape().to_vec(),
1496        a.host_data()
1497            .map_err(|e| anyhow!("triangular solve input host access failed: {e}"))?
1498            .to_vec(),
1499    )
1500    .map_err(|e| anyhow!("triangular solve lhs tensor construction failed: {e}"))?;
1501    let b_tensor = T::into_tensor(
1502        b.shape().to_vec(),
1503        b.host_data()
1504            .map_err(|e| anyhow!("triangular solve rhs host access failed: {e}"))?
1505            .to_vec(),
1506    )
1507    .map_err(|e| anyhow!("triangular solve rhs tensor construction failed: {e}"))?;
1508    let result = with_default_session(|session| {
1509        a_tensor.triangular_solve(
1510            &b_tensor,
1511            left_side,
1512            lower,
1513            transpose_a,
1514            unit_diagonal,
1515            session,
1516        )
1517    })
1518    .map_err(|e| anyhow!("triangular solve failed via tenferro-tensor: {e}"))?;
1519    try_into_typed_result::<T>("triangular_solve", result).map_err(BackendLinalgError::from)
1520}
1521
1522/// Solve `A X = B` for column-major [`Matrix`] values.
1523/// This routes the operation through the configured tenferro backend and keeps
1524/// matrix-to-tensor conversion centralized in `tensor4all-tensorbackend`.
1525/// # Errors
1526///
1527/// Returns a [`BackendLinalgError`] when the context rejects the entry
1528/// or the solve fails (for example a singular `a`).
1529///
1530/// # Examples
1531/// ```
1532/// use tensor4all_tensorbackend::{from_vec2d, solve_matrix};
1533/// let a = from_vec2d(vec![vec![2.0_f64, 1.0], vec![1.0, 2.0]]);
1534/// let b = from_vec2d(vec![vec![1.0_f64], vec![0.0]]);
1535/// let x = solve_matrix(&a, &b).unwrap();
1536/// assert!((x[[0, 0]] - 2.0 / 3.0).abs() < 1.0e-12);
1537/// assert!((x[[1, 0]] + 1.0 / 3.0).abs() < 1.0e-12);
1538/// ```
1539pub fn solve_matrix<T>(
1540    a: &Matrix<T>,
1541    b: &Matrix<T>,
1542) -> std::result::Result<Matrix<T>, BackendLinalgError>
1543where
1544    T: MatrixSolveScalar,
1545{
1546    T::solve_matrix_impl(a, b).map_err(BackendLinalgError::from)
1547}
1548
1549/// Solve `A X = B` while consuming column-major [`Matrix`] values.
1550/// This routes the operation through the configured tenferro backend and reuses
1551/// the input buffers when constructing backend tensors for directly supported
1552/// scalar types.
1553/// # Errors
1554///
1555/// Returns a [`BackendLinalgError`] when the context rejects the entry
1556/// or the solve fails (for example a singular `a`).
1557///
1558/// # Examples
1559/// ```
1560/// use tensor4all_tensorbackend::{from_vec2d, solve_matrix_owned};
1561/// let a = from_vec2d(vec![vec![2.0_f64, 1.0], vec![1.0, 2.0]]);
1562/// let b = from_vec2d(vec![vec![1.0_f64], vec![0.0]]);
1563/// let x = solve_matrix_owned(a, b).unwrap();
1564/// assert!((x[[0, 0]] - 2.0 / 3.0).abs() < 1.0e-12);
1565/// assert!((x[[1, 0]] + 1.0 / 3.0).abs() < 1.0e-12);
1566/// ```
1567pub fn solve_matrix_owned<T>(
1568    a: Matrix<T>,
1569    b: Matrix<T>,
1570) -> std::result::Result<Matrix<T>, BackendLinalgError>
1571where
1572    T: MatrixSolveScalar,
1573{
1574    T::solve_matrix_owned_impl(a, b).map_err(BackendLinalgError::from)
1575}
1576
1577/// Solve a triangular system for column-major [`Matrix`] values.
1578/// If `left_side` is true, this solves `op(A) X = B`; otherwise it solves
1579/// `X op(A) = B`. `lower` selects the triangular half, `transpose_a` applies
1580/// a transpose to `A`, and `unit_diagonal` treats the diagonal of `A` as ones.
1581/// # Errors
1582///
1583/// Returns a [`BackendLinalgError`] when the context rejects the entry
1584/// or the triangular solve fails (for example a singular `a` or inconsistent
1585/// triangular flags).
1586///
1587/// # Examples
1588/// ```
1589/// use tensor4all_tensorbackend::{from_vec2d, triangular_solve_matrix};
1590/// let a = from_vec2d(vec![vec![2.0_f64, 1.0], vec![0.0, 3.0]]);
1591/// let b = from_vec2d(vec![vec![2.0_f64, 7.0]]);
1592/// let x = triangular_solve_matrix(&a, &b, false, false, false, false).unwrap();
1593/// assert!((x[[0, 0]] - 1.0).abs() < 1.0e-12);
1594/// assert!((x[[0, 1]] - 2.0).abs() < 1.0e-12);
1595/// ```
1596pub fn triangular_solve_matrix<T>(
1597    a: &Matrix<T>,
1598    b: &Matrix<T>,
1599    left_side: bool,
1600    lower: bool,
1601    transpose_a: bool,
1602    unit_diagonal: bool,
1603) -> std::result::Result<Matrix<T>, BackendLinalgError>
1604where
1605    T: MatrixTriangularSolveScalar,
1606{
1607    T::triangular_solve_matrix_impl(a, b, left_side, lower, transpose_a, unit_diagonal)
1608        .map_err(BackendLinalgError::from)
1609}
1610
1611/// Solve a triangular system while consuming column-major [`Matrix`] values.
1612/// If `left_side` is true, this solves `op(A) X = B`; otherwise it solves
1613/// `X op(A) = B`. `lower` selects the triangular half, `transpose_a` applies
1614/// a transpose to `A`, and `unit_diagonal` treats the diagonal of `A` as ones.
1615/// # Errors
1616///
1617/// Returns a [`BackendLinalgError`] when the context rejects the entry
1618/// or the triangular solve fails (for example a singular `a` or inconsistent
1619/// triangular flags).
1620///
1621/// # Examples
1622/// ```
1623/// use tensor4all_tensorbackend::{from_vec2d, triangular_solve_matrix_owned};
1624/// let a = from_vec2d(vec![vec![2.0_f64, 0.0], vec![1.0, 3.0]]);
1625/// let b = from_vec2d(vec![vec![2.0_f64], vec![7.0]]);
1626/// let x = triangular_solve_matrix_owned(a, b, true, true, false, false).unwrap();
1627/// assert!((x[[0, 0]] - 1.0).abs() < 1.0e-12);
1628/// assert!((x[[1, 0]] - 2.0).abs() < 1.0e-12);
1629/// ```
1630pub fn triangular_solve_matrix_owned<T>(
1631    a: Matrix<T>,
1632    b: Matrix<T>,
1633    left_side: bool,
1634    lower: bool,
1635    transpose_a: bool,
1636    unit_diagonal: bool,
1637) -> std::result::Result<Matrix<T>, BackendLinalgError>
1638where
1639    T: MatrixTriangularSolveScalar,
1640{
1641    T::triangular_solve_matrix_owned_impl(a, b, left_side, lower, transpose_a, unit_diagonal)
1642        .map_err(BackendLinalgError::from)
1643}
1644
1645fn full_piv_lu_tensor<T>(
1646    tensor: Tensor,
1647) -> std::result::Result<FullPivLuResult<T>, BackendLinalgError>
1648where
1649    T: BackendLinalgScalar,
1650{
1651    with_default_session(|session| Ok(full_piv_lu_tensor_in(session, tensor)))
1652        .map_err(|e| anyhow!("complete-pivoting LU failed via tenferro-tensor: {e}"))?
1653}
1654
1655/// Complete-pivoting LU on a caller-held engine.
1656fn full_piv_lu_tensor_in<T>(
1657    session: &mut dyn tenferro_tensor::Engine,
1658    tensor: Tensor,
1659) -> std::result::Result<FullPivLuResult<T>, BackendLinalgError>
1660where
1661    T: BackendLinalgScalar,
1662{
1663    use tenferro_linalg::TensorLinalgExt;
1664
1665    let (p, l, u, q, _parity) = tensor
1666        .full_piv_lu(session)
1667        .map_err(|e| anyhow!("complete-pivoting LU failed via tenferro-tensor: {e}"))?;
1668    Ok(FullPivLuResult {
1669        p: require_host_linalg_tensor("full_piv_lu", convert_for_typed::<T>("full_piv_lu", p)?)?,
1670        l: require_host_linalg_tensor("full_piv_lu", convert_for_typed::<T>("full_piv_lu", l)?)?,
1671        u: require_host_linalg_tensor("full_piv_lu", convert_for_typed::<T>("full_piv_lu", u)?)?,
1672        q: require_host_linalg_tensor("full_piv_lu", convert_for_typed::<T>("full_piv_lu", q)?)?,
1673    })
1674}
1675
1676/// Compute complete-pivoting LU with the configured tenferro backend.
1677/// # Errors
1678///
1679/// Returns a [`BackendLinalgError`] when `a` is not host-backed, when the
1680/// context rejects the entry, or when the factorization fails.
1681///
1682pub fn full_piv_lu_backend<T>(
1683    a: &TypedTensor<T>,
1684) -> std::result::Result<FullPivLuResult<T>, BackendLinalgError>
1685where
1686    T: BackendLinalgScalar,
1687{
1688    let tensor = T::into_tensor(
1689        a.shape().to_vec(),
1690        a.host_data()
1691            .map_err(|e| anyhow!("LU input host access failed: {e}"))?
1692            .to_vec(),
1693    )
1694    .map_err(|e| anyhow!("LU input tensor construction failed: {e}"))?;
1695    full_piv_lu_tensor(tensor)
1696}
1697
1698/// Complete-pivoting LU of a typed tensor on a caller-held engine.
1699///
1700/// Same kernel and conversion as [`full_piv_lu_backend`], entered through the given
1701/// session instead of the process-global one.
1702///
1703/// # Errors
1704///
1705/// Returns [`BackendLinalgError`] under the same conditions as
1706/// [`full_piv_lu_backend`].
1707pub fn full_piv_lu_backend_in<T>(
1708    session: &mut dyn tenferro_tensor::Engine,
1709    a: &TypedTensor<T>,
1710) -> std::result::Result<FullPivLuResult<T>, BackendLinalgError>
1711where
1712    T: BackendLinalgScalar,
1713{
1714    let tensor = T::into_tensor(
1715        a.shape().to_vec(),
1716        a.host_data()
1717            .map_err(|e| anyhow!("LU input host access failed: {e}"))?
1718            .to_vec(),
1719    )
1720    .map_err(|e| anyhow!("LU input tensor construction failed: {e}"))?;
1721    full_piv_lu_tensor_in(session, tensor)
1722}
1723
1724/// Compute complete-pivoting LU for a column-major [`Matrix`].
1725/// This is a convenience wrapper over [`full_piv_lu_backend`] for callers that
1726/// use [`Matrix`] as their dense boundary type.
1727/// # Errors
1728///
1729/// Returns a [`BackendLinalgError`] when the context rejects the entry
1730/// or the full-pivoting LU factorization fails.
1731///
1732/// # Examples
1733/// ```
1734/// use tensor4all_tensorbackend::{from_vec2d, full_piv_lu_matrix};
1735/// let matrix = from_vec2d(vec![vec![0.0_f64, 1.0], vec![2.0, 3.0]]);
1736/// let factors = full_piv_lu_matrix(&matrix).unwrap();
1737/// assert_eq!(factors.p.nrows(), 2);
1738/// assert_eq!(factors.q.ncols(), 2);
1739/// ```
1740pub fn full_piv_lu_matrix<T>(
1741    a: &Matrix<T>,
1742) -> std::result::Result<FullPivLuMatrixResult<T>, BackendLinalgError>
1743where
1744    T: BackendLinalgScalar + Copy,
1745{
1746    let tensor = matrix_to_typed_tensor(a);
1747    let decomp = full_piv_lu_backend(&tensor)?;
1748    Ok(FullPivLuMatrixResult {
1749        p: typed_tensor_to_matrix("full_piv_lu", decomp.p)?,
1750        l: typed_tensor_to_matrix("full_piv_lu", decomp.l)?,
1751        u: typed_tensor_to_matrix("full_piv_lu", decomp.u)?,
1752        q: typed_tensor_to_matrix("full_piv_lu", decomp.q)?,
1753    })
1754}
1755
1756/// Complete-pivoting LU for a column-major [`Matrix`] on a caller-held engine.
1757///
1758/// Same kernel and conversion as [`full_piv_lu_matrix`], entered through the given
1759/// session instead of the process-global one.
1760///
1761/// # Errors
1762///
1763/// Returns [`BackendLinalgError`] under the same conditions as
1764/// [`full_piv_lu_matrix`].
1765pub fn full_piv_lu_matrix_in<T>(
1766    session: &mut dyn tenferro_tensor::Engine,
1767    a: &Matrix<T>,
1768) -> std::result::Result<FullPivLuMatrixResult<T>, BackendLinalgError>
1769where
1770    T: BackendLinalgScalar + Copy,
1771{
1772    let tensor = matrix_to_typed_tensor(a);
1773    let decomp = full_piv_lu_backend_in(session, &tensor)?;
1774    Ok(FullPivLuMatrixResult {
1775        p: typed_tensor_to_matrix("full_piv_lu", decomp.p)?,
1776        l: typed_tensor_to_matrix("full_piv_lu", decomp.l)?,
1777        u: typed_tensor_to_matrix("full_piv_lu", decomp.u)?,
1778        q: typed_tensor_to_matrix("full_piv_lu", decomp.q)?,
1779    })
1780}
1781
1782/// Compute complete-pivoting LU for an owned column-major [`Matrix`].
1783///
1784/// This is the owned-buffer counterpart of [`full_piv_lu_matrix`]. It consumes
1785/// the matrix so its column-major buffer can be transferred to tenferro without
1786/// cloning the input before factorization.
1787/// The returned factors satisfy `P * A * Qᵀ = L * U` when interpreted as
1788/// column-major matrices.
1789///
1790/// # Errors
1791///
1792/// Returns [`BackendLinalgError`] when the input is not square (tenferro
1793/// reports an incompatible shape), the configured backend rejects the scalar
1794/// dtype, the complete-pivoting factorization fails, or a factor produced by
1795/// the backend cannot be converted back to a matrix.
1796///
1797/// # Examples
1798/// ```
1799/// use tensor4all_tensorbackend::{from_vec2d, full_piv_lu_matrix_owned, Matrix};
1800///
1801/// fn matmul(a: &Matrix<f64>, b: &Matrix<f64>) -> Matrix<f64> {
1802///     let mut out = Matrix::zeros(a.nrows(), b.ncols());
1803///     for col in 0..b.ncols() {
1804///         for k in 0..a.ncols() {
1805///             for row in 0..a.nrows() {
1806///                 out[[row, col]] += a[[row, k]] * b[[k, col]];
1807///             }
1808///         }
1809///     }
1810///     out
1811/// }
1812///
1813/// fn transpose(a: &Matrix<f64>) -> Matrix<f64> {
1814///     let mut out = Matrix::zeros(a.ncols(), a.nrows());
1815///     for col in 0..a.ncols() {
1816///         for row in 0..a.nrows() {
1817///             out[[col, row]] = a[[row, col]];
1818///         }
1819///     }
1820///     out
1821/// }
1822///
1823/// let matrix = from_vec2d(vec![vec![2.0_f64, 1.0], vec![1.0, 2.0]]);
1824/// let factors = full_piv_lu_matrix_owned(matrix.clone()).unwrap();
1825/// let lhs = matmul(&factors.p, &matmul(&matrix, &transpose(&factors.q)));
1826/// let rhs = matmul(&factors.l, &factors.u);
1827/// for row in 0..2 {
1828///     for col in 0..2 {
1829///         assert!((lhs[[row, col]] - rhs[[row, col]]).abs() < 1.0e-12);
1830///     }
1831/// }
1832/// ```
1833pub fn full_piv_lu_matrix_owned<T>(
1834    a: Matrix<T>,
1835) -> std::result::Result<FullPivLuMatrixResult<T>, BackendLinalgError>
1836where
1837    T: BackendLinalgScalar + Copy,
1838{
1839    let tensor = T::typed_tensor_into_tensor(a.into_typed_tensor());
1840    let decomp = full_piv_lu_tensor(tensor)?;
1841    Ok(FullPivLuMatrixResult {
1842        p: typed_tensor_to_matrix("full_piv_lu", decomp.p)?,
1843        l: typed_tensor_to_matrix("full_piv_lu", decomp.l)?,
1844        u: typed_tensor_to_matrix("full_piv_lu", decomp.u)?,
1845        q: typed_tensor_to_matrix("full_piv_lu", decomp.q)?,
1846    })
1847}
1848
1849#[cfg(test)]
1850mod tests;