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};
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
354fn solve_matrix_direct<T>(a: &Matrix<T>, b: &Matrix<T>) -> Result<Matrix<T>>
355where
356    T: BackendLinalgScalar + Copy,
357    Tensor: From<TypedTensor<T>>,
358{
359    solve_matrix_direct_owned(a.clone(), b.clone())
360}
361
362fn solve_matrix_direct_owned<T>(a: Matrix<T>, b: Matrix<T>) -> Result<Matrix<T>>
363where
364    T: BackendLinalgScalar + Copy,
365    Tensor: From<TypedTensor<T>>,
366{
367    let a_tensor: Tensor = a.into_typed_tensor().into();
368    let b_tensor: Tensor = b.into_typed_tensor().into();
369    let result = with_default_session(|session| a_tensor.solve(&b_tensor, session))
370        .map_err(|e| anyhow!("linear solve failed via tenferro-tensor: {e}"))?;
371    let x = try_into_typed_result::<T>("solve", result)?;
372    typed_tensor_to_matrix("solve", x)
373}
374
375fn triangular_solve_matrix_direct<T>(
376    a: &Matrix<T>,
377    b: &Matrix<T>,
378    left_side: bool,
379    lower: bool,
380    transpose_a: bool,
381    unit_diagonal: bool,
382) -> Result<Matrix<T>>
383where
384    T: BackendLinalgScalar + Copy,
385    Tensor: From<TypedTensor<T>>,
386{
387    triangular_solve_matrix_direct_owned(
388        a.clone(),
389        b.clone(),
390        left_side,
391        lower,
392        transpose_a,
393        unit_diagonal,
394    )
395}
396
397fn triangular_solve_matrix_direct_owned<T>(
398    a: Matrix<T>,
399    b: Matrix<T>,
400    left_side: bool,
401    lower: bool,
402    transpose_a: bool,
403    unit_diagonal: bool,
404) -> Result<Matrix<T>>
405where
406    T: BackendLinalgScalar + Copy,
407    Tensor: From<TypedTensor<T>>,
408{
409    let a_tensor: Tensor = a.into_typed_tensor().into();
410    let b_tensor: Tensor = b.into_typed_tensor().into();
411    let result = with_default_session(|session| {
412        a_tensor.triangular_solve(
413            &b_tensor,
414            left_side,
415            lower,
416            transpose_a,
417            unit_diagonal,
418            session,
419        )
420    })
421    .map_err(|e| anyhow!("triangular solve failed via tenferro-tensor: {e}"))?;
422    let x = try_into_typed_result::<T>("triangular_solve", result)?;
423    typed_tensor_to_matrix("triangular_solve", x)
424}
425
426impl MatrixSolveScalar for f64 {
427    fn solve_matrix_impl(a: &Matrix<Self>, b: &Matrix<Self>) -> Result<Matrix<Self>> {
428        solve_matrix_direct(a, b)
429    }
430
431    fn solve_matrix_owned_impl(a: Matrix<Self>, b: Matrix<Self>) -> Result<Matrix<Self>> {
432        solve_matrix_direct_owned(a, b)
433    }
434}
435
436impl MatrixTriangularSolveScalar for f64 {
437    fn triangular_solve_matrix_impl(
438        a: &Matrix<Self>,
439        b: &Matrix<Self>,
440        left_side: bool,
441        lower: bool,
442        transpose_a: bool,
443        unit_diagonal: bool,
444    ) -> Result<Matrix<Self>> {
445        triangular_solve_matrix_direct(a, b, left_side, lower, transpose_a, unit_diagonal)
446    }
447
448    fn triangular_solve_matrix_owned_impl(
449        a: Matrix<Self>,
450        b: Matrix<Self>,
451        left_side: bool,
452        lower: bool,
453        transpose_a: bool,
454        unit_diagonal: bool,
455    ) -> Result<Matrix<Self>> {
456        triangular_solve_matrix_direct_owned(a, b, left_side, lower, transpose_a, unit_diagonal)
457    }
458}
459
460impl MatrixSolveScalar for Complex64 {
461    fn solve_matrix_impl(a: &Matrix<Self>, b: &Matrix<Self>) -> Result<Matrix<Self>> {
462        solve_matrix_direct(a, b)
463    }
464
465    fn solve_matrix_owned_impl(a: Matrix<Self>, b: Matrix<Self>) -> Result<Matrix<Self>> {
466        solve_matrix_direct_owned(a, b)
467    }
468}
469
470impl MatrixTriangularSolveScalar for Complex64 {
471    fn triangular_solve_matrix_impl(
472        a: &Matrix<Self>,
473        b: &Matrix<Self>,
474        left_side: bool,
475        lower: bool,
476        transpose_a: bool,
477        unit_diagonal: bool,
478    ) -> Result<Matrix<Self>> {
479        triangular_solve_matrix_direct(a, b, left_side, lower, transpose_a, unit_diagonal)
480    }
481
482    fn triangular_solve_matrix_owned_impl(
483        a: Matrix<Self>,
484        b: Matrix<Self>,
485        left_side: bool,
486        lower: bool,
487        transpose_a: bool,
488        unit_diagonal: bool,
489    ) -> Result<Matrix<Self>> {
490        triangular_solve_matrix_direct_owned(a, b, left_side, lower, transpose_a, unit_diagonal)
491    }
492}
493
494impl MatrixSolveScalar for f32 {
495    fn solve_matrix_impl(a: &Matrix<Self>, b: &Matrix<Self>) -> Result<Matrix<Self>> {
496        let a64 = Matrix::from_col_major_vec(
497            a.nrows(),
498            a.ncols(),
499            a.as_col_major_slice()
500                .iter()
501                .map(|&value| value as f64)
502                .collect(),
503        );
504        let b64 = Matrix::from_col_major_vec(
505            b.nrows(),
506            b.ncols(),
507            b.as_col_major_slice()
508                .iter()
509                .map(|&value| value as f64)
510                .collect(),
511        );
512        let x64 = solve_matrix_direct(&a64, &b64)?;
513        Ok(Matrix::from_col_major_vec(
514            x64.nrows(),
515            x64.ncols(),
516            x64.as_col_major_slice()
517                .iter()
518                .map(|&value| value as f32)
519                .collect(),
520        ))
521    }
522}
523
524impl MatrixTriangularSolveScalar for f32 {
525    fn triangular_solve_matrix_impl(
526        a: &Matrix<Self>,
527        b: &Matrix<Self>,
528        left_side: bool,
529        lower: bool,
530        transpose_a: bool,
531        unit_diagonal: bool,
532    ) -> Result<Matrix<Self>> {
533        let a64 = Matrix::from_col_major_vec(
534            a.nrows(),
535            a.ncols(),
536            a.as_col_major_slice()
537                .iter()
538                .map(|&value| value as f64)
539                .collect(),
540        );
541        let b64 = Matrix::from_col_major_vec(
542            b.nrows(),
543            b.ncols(),
544            b.as_col_major_slice()
545                .iter()
546                .map(|&value| value as f64)
547                .collect(),
548        );
549        let x64 = triangular_solve_matrix_direct(
550            &a64,
551            &b64,
552            left_side,
553            lower,
554            transpose_a,
555            unit_diagonal,
556        )?;
557        Ok(Matrix::from_col_major_vec(
558            x64.nrows(),
559            x64.ncols(),
560            x64.as_col_major_slice()
561                .iter()
562                .map(|&value| value as f32)
563                .collect(),
564        ))
565    }
566}
567
568impl MatrixSolveScalar for Complex32 {
569    fn solve_matrix_impl(a: &Matrix<Self>, b: &Matrix<Self>) -> Result<Matrix<Self>> {
570        let a64 = Matrix::from_col_major_vec(
571            a.nrows(),
572            a.ncols(),
573            a.as_col_major_slice()
574                .iter()
575                .map(|&value| Complex64::new(value.re as f64, value.im as f64))
576                .collect(),
577        );
578        let b64 = Matrix::from_col_major_vec(
579            b.nrows(),
580            b.ncols(),
581            b.as_col_major_slice()
582                .iter()
583                .map(|&value| Complex64::new(value.re as f64, value.im as f64))
584                .collect(),
585        );
586        let x64 = solve_matrix_direct(&a64, &b64)?;
587        Ok(Matrix::from_col_major_vec(
588            x64.nrows(),
589            x64.ncols(),
590            x64.as_col_major_slice()
591                .iter()
592                .map(|&value| Complex32::new(value.re as f32, value.im as f32))
593                .collect(),
594        ))
595    }
596}
597
598impl MatrixTriangularSolveScalar for Complex32 {
599    fn triangular_solve_matrix_impl(
600        a: &Matrix<Self>,
601        b: &Matrix<Self>,
602        left_side: bool,
603        lower: bool,
604        transpose_a: bool,
605        unit_diagonal: bool,
606    ) -> Result<Matrix<Self>> {
607        let a64 = Matrix::from_col_major_vec(
608            a.nrows(),
609            a.ncols(),
610            a.as_col_major_slice()
611                .iter()
612                .map(|&value| Complex64::new(value.re as f64, value.im as f64))
613                .collect(),
614        );
615        let b64 = Matrix::from_col_major_vec(
616            b.nrows(),
617            b.ncols(),
618            b.as_col_major_slice()
619                .iter()
620                .map(|&value| Complex64::new(value.re as f64, value.im as f64))
621                .collect(),
622        );
623        let x64 = triangular_solve_matrix_direct(
624            &a64,
625            &b64,
626            left_side,
627            lower,
628            transpose_a,
629            unit_diagonal,
630        )?;
631        Ok(Matrix::from_col_major_vec(
632            x64.nrows(),
633            x64.ncols(),
634            x64.as_col_major_slice()
635                .iter()
636                .map(|&value| Complex32::new(value.re as f32, value.im as f32))
637                .collect(),
638        ))
639    }
640}
641
642fn tensor_scalar_dtype<T: TensorScalar>() -> DType {
643    T::dtype()
644}
645
646fn try_into_typed_result<T: TensorScalar>(
647    op: &'static str,
648    tensor: Tensor,
649) -> Result<TypedTensor<T>> {
650    let actual = tensor.dtype();
651    T::into_typed(tensor).map_err(|source| {
652        anyhow!(
653            "{op}: dtype mismatch lhs={actual:?} rhs={:?}: {source}",
654            tensor_scalar_dtype::<T>()
655        )
656    })
657}
658
659fn convert_for_typed<T: TensorScalar>(op: &'static str, tensor: Tensor) -> Result<TypedTensor<T>> {
660    let expected = tensor_scalar_dtype::<T>();
661    let tensor = if tensor.dtype() == expected {
662        tensor
663    } else {
664        with_default_session(|session| tensor.convert(expected, session))
665            .map_err(|e| anyhow!("{op}: dtype conversion to {expected:?} failed: {e}"))?
666    };
667    try_into_typed_result::<T>(op, tensor)
668}
669
670fn matrix_to_typed_tensor<T>(matrix: &Matrix<T>) -> TypedTensor<T>
671where
672    T: TensorScalar + Copy,
673{
674    crate::require_invariant(
675        TypedTensor::from_vec_col_major(
676            vec![matrix.nrows(), matrix.ncols()],
677            matrix.as_col_major_slice().to_vec(),
678        ),
679        "validated matrix rejected by tenferro",
680    )
681}
682
683fn typed_tensor_to_matrix<T>(op: &'static str, tensor: TypedTensor<T>) -> Result<Matrix<T>>
684where
685    T: TensorScalar + Copy,
686{
687    Matrix::try_from_typed_tensor(tensor).map_err(|err| anyhow!("{op}: {err}"))
688}
689
690fn require_host_linalg_tensor<T: TensorScalar>(
691    op: &'static str,
692    tensor: TypedTensor<T>,
693) -> Result<TypedTensor<T>> {
694    tensor
695        .host_data()
696        .map_err(|error| anyhow!("{op}: result must be host-backed: {error}"))?;
697    Ok(tensor)
698}
699
700/// Compute a thin/economy SVD on a typed tensor.
701/// # Errors
702///
703/// Returns an error when the SVD fails (a backend or non-convergence
704/// /// failure).
705///
706pub fn svd_backend<T>(a: &TypedTensor<T>) -> std::result::Result<SvdResult<T>, BackendLinalgError>
707where
708    T: BackendLinalgScalar,
709{
710    let tensor = T::into_tensor(
711        a.shape().to_vec(),
712        a.host_data()
713            .map_err(|e| anyhow!("SVD input host access failed: {e}"))?
714            .to_vec(),
715    )
716    .map_err(|e| anyhow!("SVD input tensor construction failed: {e}"))?;
717    let (u, s, vt) = with_default_session(|session| tensor.svd(session))
718        .map_err(|e| anyhow!("SVD computation failed via tenferro-tensor: {e}"))?;
719    Ok(SvdResult {
720        u: require_host_linalg_tensor("svd", convert_for_typed::<T>("svd", u)?)?,
721        s: require_host_linalg_tensor("svd", convert_for_typed::<T::Real>("svd", s)?)?,
722        vt: require_host_linalg_tensor("svd", convert_for_typed::<T>("svd", vt)?)?,
723    })
724}
725
726/// Compute a thin/economy QR decomposition on a typed tensor.
727/// # Errors
728///
729/// Returns an error when the QR fails (a backend or non-convergence
730/// /// failure).
731///
732pub fn qr_backend<T>(
733    a: &TypedTensor<T>,
734) -> std::result::Result<(TypedTensor<T>, TypedTensor<T>), BackendLinalgError>
735where
736    T: BackendLinalgScalar,
737{
738    let tensor = T::into_tensor(
739        a.shape().to_vec(),
740        a.host_data()
741            .map_err(|e| anyhow!("QR input host access failed: {e}"))?
742            .to_vec(),
743    )
744    .map_err(|e| anyhow!("QR input tensor construction failed: {e}"))?;
745    let (q, r) = with_default_session(|session| tensor.qr(session))
746        .map_err(|e| anyhow!("QR computation failed via tenferro-tensor: {e}"))?;
747    Ok((
748        convert_for_typed::<T>("qr", q)?,
749        convert_for_typed::<T>("qr", r)?,
750    ))
751}
752
753/// Solve `A X = B` with the configured tenferro backend.
754/// # Errors
755/// Returns an error when the input shapes or scalar dtype are invalid (a
756/// shape or dtype mismatch) or the coefficient matrix is singular (a singular
757/// failure).
758pub fn solve_backend<T>(
759    a: &TypedTensor<T>,
760    b: &TypedTensor<T>,
761) -> std::result::Result<TypedTensor<T>, BackendLinalgError>
762where
763    T: BackendLinalgScalar,
764{
765    let a_tensor = T::into_tensor(
766        a.shape().to_vec(),
767        a.host_data()
768            .map_err(|e| anyhow!("solve input host access failed: {e}"))?
769            .to_vec(),
770    )
771    .map_err(|e| anyhow!("solve lhs tensor construction failed: {e}"))?;
772    let b_tensor = T::into_tensor(
773        b.shape().to_vec(),
774        b.host_data()
775            .map_err(|e| anyhow!("solve rhs host access failed: {e}"))?
776            .to_vec(),
777    )
778    .map_err(|e| anyhow!("solve rhs tensor construction failed: {e}"))?;
779    let result = with_default_session(|session| a_tensor.solve(&b_tensor, session))
780        .map_err(|e| anyhow!("linear solve failed via tenferro-tensor: {e}"))?;
781    try_into_typed_result::<T>("solve", result).map_err(BackendLinalgError::from)
782}
783
784/// Solve a triangular system with the configured tenferro backend.
785/// If `left_side` is true, this solves `op(A) X = B`; otherwise it solves
786/// `X op(A) = B`. `lower` selects the triangular half, `transpose_a` applies
787/// a transpose to `A`, and `unit_diagonal` treats the diagonal of `A` as ones.
788/// # Errors
789///
790/// Returns an error when the solve fails (a backend, singular, or shape
791/// /// mismatch failure).
792///
793pub fn triangular_solve_backend<T>(
794    a: &TypedTensor<T>,
795    b: &TypedTensor<T>,
796    left_side: bool,
797    lower: bool,
798    transpose_a: bool,
799    unit_diagonal: bool,
800) -> std::result::Result<TypedTensor<T>, BackendLinalgError>
801where
802    T: BackendLinalgScalar,
803{
804    let a_tensor = T::into_tensor(
805        a.shape().to_vec(),
806        a.host_data()
807            .map_err(|e| anyhow!("triangular solve input host access failed: {e}"))?
808            .to_vec(),
809    )
810    .map_err(|e| anyhow!("triangular solve lhs tensor construction failed: {e}"))?;
811    let b_tensor = T::into_tensor(
812        b.shape().to_vec(),
813        b.host_data()
814            .map_err(|e| anyhow!("triangular solve rhs host access failed: {e}"))?
815            .to_vec(),
816    )
817    .map_err(|e| anyhow!("triangular solve rhs tensor construction failed: {e}"))?;
818    let result = with_default_session(|session| {
819        a_tensor.triangular_solve(
820            &b_tensor,
821            left_side,
822            lower,
823            transpose_a,
824            unit_diagonal,
825            session,
826        )
827    })
828    .map_err(|e| anyhow!("triangular solve failed via tenferro-tensor: {e}"))?;
829    try_into_typed_result::<T>("triangular_solve", result).map_err(BackendLinalgError::from)
830}
831
832/// Solve `A X = B` for column-major [`Matrix`] values.
833/// This routes the operation through the configured tenferro backend and keeps
834/// matrix-to-tensor conversion centralized in `tensor4all-tensorbackend`.
835/// # Errors
836///
837/// Returns an error when the input shapes or scalar dtype are invalid (a
838/// /// shape or dtype mismatch) or the solve fails (a backend or singular
839/// /// failure).
840///
841/// # Examples
842/// ```
843/// use tensor4all_tensorbackend::{from_vec2d, solve_matrix};
844/// let a = from_vec2d(vec![vec![2.0_f64, 1.0], vec![1.0, 2.0]]);
845/// let b = from_vec2d(vec![vec![1.0_f64], vec![0.0]]);
846/// let x = solve_matrix(&a, &b).unwrap();
847/// assert!((x[[0, 0]] - 2.0 / 3.0).abs() < 1.0e-12);
848/// assert!((x[[1, 0]] + 1.0 / 3.0).abs() < 1.0e-12);
849/// ```
850pub fn solve_matrix<T>(
851    a: &Matrix<T>,
852    b: &Matrix<T>,
853) -> std::result::Result<Matrix<T>, BackendLinalgError>
854where
855    T: MatrixSolveScalar,
856{
857    T::solve_matrix_impl(a, b).map_err(BackendLinalgError::from)
858}
859
860/// Solve `A X = B` while consuming column-major [`Matrix`] values.
861/// This routes the operation through the configured tenferro backend and reuses
862/// the input buffers when constructing backend tensors for directly supported
863/// scalar types.
864/// # Errors
865///
866/// Returns an error when the input shapes or scalar dtype are invalid (a
867/// /// shape or dtype mismatch) or the solve fails (a backend or singular
868/// /// failure).
869///
870/// # Examples
871/// ```
872/// use tensor4all_tensorbackend::{from_vec2d, solve_matrix_owned};
873/// let a = from_vec2d(vec![vec![2.0_f64, 1.0], vec![1.0, 2.0]]);
874/// let b = from_vec2d(vec![vec![1.0_f64], vec![0.0]]);
875/// let x = solve_matrix_owned(a, b).unwrap();
876/// assert!((x[[0, 0]] - 2.0 / 3.0).abs() < 1.0e-12);
877/// assert!((x[[1, 0]] + 1.0 / 3.0).abs() < 1.0e-12);
878/// ```
879pub fn solve_matrix_owned<T>(
880    a: Matrix<T>,
881    b: Matrix<T>,
882) -> std::result::Result<Matrix<T>, BackendLinalgError>
883where
884    T: MatrixSolveScalar,
885{
886    T::solve_matrix_owned_impl(a, b).map_err(BackendLinalgError::from)
887}
888
889/// Solve a triangular system for column-major [`Matrix`] values.
890/// If `left_side` is true, this solves `op(A) X = B`; otherwise it solves
891/// `X op(A) = B`. `lower` selects the triangular half, `transpose_a` applies
892/// a transpose to `A`, and `unit_diagonal` treats the diagonal of `A` as ones.
893/// # Errors
894///
895/// Returns an error when the input shapes or scalar dtype are invalid (a
896/// /// shape or dtype mismatch), the triangular flags are invalid (an
897/// /// invalid-configuration failure), or the solve fails (a backend or singular
898/// /// failure).
899///
900/// # Examples
901/// ```
902/// use tensor4all_tensorbackend::{from_vec2d, triangular_solve_matrix};
903/// let a = from_vec2d(vec![vec![2.0_f64, 1.0], vec![0.0, 3.0]]);
904/// let b = from_vec2d(vec![vec![2.0_f64, 7.0]]);
905/// let x = triangular_solve_matrix(&a, &b, false, false, false, false).unwrap();
906/// assert!((x[[0, 0]] - 1.0).abs() < 1.0e-12);
907/// assert!((x[[0, 1]] - 2.0).abs() < 1.0e-12);
908/// ```
909pub fn triangular_solve_matrix<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) -> std::result::Result<Matrix<T>, BackendLinalgError>
917where
918    T: MatrixTriangularSolveScalar,
919{
920    T::triangular_solve_matrix_impl(a, b, left_side, lower, transpose_a, unit_diagonal)
921        .map_err(BackendLinalgError::from)
922}
923
924/// Solve a triangular system while consuming column-major [`Matrix`] values.
925/// If `left_side` is true, this solves `op(A) X = B`; otherwise it solves
926/// `X op(A) = B`. `lower` selects the triangular half, `transpose_a` applies
927/// a transpose to `A`, and `unit_diagonal` treats the diagonal of `A` as ones.
928/// # Errors
929///
930/// Returns an error when the input shapes or scalar dtype are invalid (a
931/// /// shape or dtype mismatch), the triangular flags are invalid (an
932/// /// invalid-configuration failure), or the solve fails (a backend or singular
933/// /// failure).
934///
935/// # Examples
936/// ```
937/// use tensor4all_tensorbackend::{from_vec2d, triangular_solve_matrix_owned};
938/// let a = from_vec2d(vec![vec![2.0_f64, 0.0], vec![1.0, 3.0]]);
939/// let b = from_vec2d(vec![vec![2.0_f64], vec![7.0]]);
940/// let x = triangular_solve_matrix_owned(a, b, true, true, false, false).unwrap();
941/// assert!((x[[0, 0]] - 1.0).abs() < 1.0e-12);
942/// assert!((x[[1, 0]] - 2.0).abs() < 1.0e-12);
943/// ```
944pub fn triangular_solve_matrix_owned<T>(
945    a: Matrix<T>,
946    b: Matrix<T>,
947    left_side: bool,
948    lower: bool,
949    transpose_a: bool,
950    unit_diagonal: bool,
951) -> std::result::Result<Matrix<T>, BackendLinalgError>
952where
953    T: MatrixTriangularSolveScalar,
954{
955    T::triangular_solve_matrix_owned_impl(a, b, left_side, lower, transpose_a, unit_diagonal)
956        .map_err(BackendLinalgError::from)
957}
958
959/// Compute complete-pivoting LU with the configured tenferro backend.
960/// # Errors
961///
962/// Returns an error when the LU factorization fails (a backend or
963/// /// non-convergence failure).
964///
965pub fn full_piv_lu_backend<T>(
966    a: &TypedTensor<T>,
967) -> std::result::Result<FullPivLuResult<T>, BackendLinalgError>
968where
969    T: BackendLinalgScalar,
970{
971    let tensor = T::into_tensor(
972        a.shape().to_vec(),
973        a.host_data()
974            .map_err(|e| anyhow!("LU input host access failed: {e}"))?
975            .to_vec(),
976    )
977    .map_err(|e| anyhow!("LU input tensor construction failed: {e}"))?;
978    let (p, l, u, q, _parity) = with_default_session(|session| tensor.full_piv_lu(session))
979        .map_err(|e| anyhow!("complete-pivoting LU failed via tenferro-tensor: {e}"))?;
980    Ok(FullPivLuResult {
981        p: require_host_linalg_tensor("full_piv_lu", convert_for_typed::<T>("full_piv_lu", p)?)?,
982        l: require_host_linalg_tensor("full_piv_lu", convert_for_typed::<T>("full_piv_lu", l)?)?,
983        u: require_host_linalg_tensor("full_piv_lu", convert_for_typed::<T>("full_piv_lu", u)?)?,
984        q: require_host_linalg_tensor("full_piv_lu", convert_for_typed::<T>("full_piv_lu", q)?)?,
985    })
986}
987
988/// Compute complete-pivoting LU for a column-major [`Matrix`].
989/// This is a convenience wrapper over [`full_piv_lu_backend`] for callers that
990/// use [`Matrix`] as their dense boundary type.
991/// # Errors
992///
993/// Returns an error when the backend does not support the input dtype (a
994/// /// dtype mismatch) or the factorization fails (a backend or
995/// /// non-convergence failure).
996///
997/// # Examples
998/// ```
999/// use tensor4all_tensorbackend::{from_vec2d, full_piv_lu_matrix};
1000/// let matrix = from_vec2d(vec![vec![0.0_f64, 1.0], vec![2.0, 3.0]]);
1001/// let factors = full_piv_lu_matrix(&matrix).unwrap();
1002/// assert_eq!(factors.p.nrows(), 2);
1003/// assert_eq!(factors.q.ncols(), 2);
1004/// ```
1005pub fn full_piv_lu_matrix<T>(
1006    a: &Matrix<T>,
1007) -> std::result::Result<FullPivLuMatrixResult<T>, BackendLinalgError>
1008where
1009    T: BackendLinalgScalar + Copy,
1010{
1011    let tensor = matrix_to_typed_tensor(a);
1012    let decomp = full_piv_lu_backend(&tensor)?;
1013    Ok(FullPivLuMatrixResult {
1014        p: typed_tensor_to_matrix("full_piv_lu", decomp.p)?,
1015        l: typed_tensor_to_matrix("full_piv_lu", decomp.l)?,
1016        u: typed_tensor_to_matrix("full_piv_lu", decomp.u)?,
1017        q: typed_tensor_to_matrix("full_piv_lu", decomp.q)?,
1018    })
1019}
1020
1021#[cfg(test)]
1022mod tests;