Skip to main content

tenferro_linalg/
backend.rs

1use tenferro_tensor::{BackendSession, Tensor, TensorRead, TensorWrite};
2
3pub(crate) use crate::error::unsupported_dtype;
4use crate::extension::{
5    apply_eigh_gauge, apply_qr_gauge, apply_svd_gauge, validate_derivative_eps, EighOptions,
6    QrOptions, SvdOptions,
7};
8
9/// Backend surface required by the linalg extension runtime.
10///
11/// # Examples
12///
13/// ```rust
14/// use tenferro_cpu::{with_cpu_exec_session, CpuBackend, CpuExecSession};
15/// use tenferro_linalg::backend::LinalgBackend;
16/// use tenferro_tensor::BackendSessionHost;
17///
18/// fn assert_linalg_backend<B: LinalgBackend>() {}
19///
20/// assert_linalg_backend::<CpuExecSession<'static>>();
21/// let mut host = CpuBackend::new();
22/// host.with_backend_session(|session| {
23///     with_cpu_exec_session(session, |_backend| ())
24///         .expect("CpuBackend must expose a CpuExecSession");
25/// });
26/// ```
27pub trait LinalgBackend: BackendSession {
28    /// Compute a Cholesky factorization.
29    ///
30    /// # Errors
31    ///
32    /// Returns `Error::Validation` for non-matrix, non-square, or unsupported
33    /// input dtypes; `Error::Extension` with `ErrorKind::NumericalFailure`
34    /// when the matrix is not positive definite; or a typed backend source
35    /// when the provider cannot execute the factorization.
36    fn cholesky(&mut self, input: &Tensor) -> tenferro_tensor::Result<Tensor>;
37
38    /// Solve a triangular linear system with explicit side, triangle,
39    /// transpose, and unit-diagonal flags.
40    ///
41    /// # Errors
42    ///
43    /// Returns `Error::Validation` for incompatible matrix/rhs shapes, rank,
44    /// or dtype; `Error::Extension` with `ErrorKind::NumericalFailure` for a
45    /// singular or zero-diagonal system; or a typed backend source for a
46    /// provider failure.
47    fn triangular_solve(
48        &mut self,
49        a: &Tensor,
50        b: &Tensor,
51        left_side: bool,
52        lower: bool,
53        transpose_a: bool,
54        unit_diagonal: bool,
55    ) -> tenferro_tensor::Result<Tensor>;
56
57    // INVARIANT: the six flags are the public triangular-solve contract and mirror the owned hook.
58    #[allow(clippy::too_many_arguments)]
59    /// Solve a triangular linear system from tensor read targets.
60    ///
61    /// Backends may canonicalize the inputs inside the same placement family,
62    /// but must not silently transfer between CPU and GPU memory.
63    ///
64    /// # Examples
65    ///
66    /// ```rust
67    /// use tenferro_cpu::{with_cpu_exec_session, CpuBackend};
68    /// use tenferro_linalg::LinalgBackend;
69    /// use tenferro_tensor::{BackendSessionHost, Tensor, TensorRead};
70    ///
71    /// let a = Tensor::from_vec_col_major(vec![2, 2], vec![2.0_f64, 0.0, 1.0, 3.0])?;
72    /// let b = Tensor::from_vec_col_major(vec![2, 1], vec![4.0_f64, 9.0])?;
73    /// let mut host = CpuBackend::new();
74    /// let x = host.with_backend_session(|session| {
75    ///     with_cpu_exec_session(session, |backend| {
76    ///         backend.triangular_solve_read(
77    ///             TensorRead::from_tensor(&a),
78    ///             TensorRead::from_tensor(&b),
79    ///             true,
80    ///             false,
81    ///             false,
82    ///             false,
83    ///         )
84    ///     })
85    ///     .expect("CpuBackend must expose a CpuExecSession")
86    /// })?;
87    /// let Tensor::F64(x) = x else { unreachable!("F64 inputs return F64 output") };
88    /// assert_eq!(x.host_data()?, &[0.5, 3.0]);
89    /// # Ok::<(), tenferro_tensor::Error>(())
90    /// ```
91    ///
92    /// # Errors
93    ///
94    /// The default implementation returns `Error::Unsupported` because the
95    /// backend does not accept tensor read targets. Implementations may return
96    /// `Error::Validation` for incompatible shapes or dtypes,
97    /// `Error::RuntimeState` for invalid placement, `Error::Extension` for a
98    /// singular system, or a typed backend-source error.
99    fn triangular_solve_read(
100        &mut self,
101        _a: TensorRead<'_>,
102        _b: TensorRead<'_>,
103        _left_side: bool,
104        _lower: bool,
105        _transpose_a: bool,
106        _unit_diagonal: bool,
107    ) -> tenferro_tensor::Result<Tensor> {
108        Err(tenferro_tensor::Error::unsupported(
109            "triangular_solve",
110            "backend does not accept tensor reads at this execution boundary",
111        ))
112    }
113
114    /// Compute public LU outputs `(P, L, U, parity)`.
115    ///
116    /// # Errors
117    ///
118    /// Returns `Error::Validation` when the input is not a supported matrix or
119    /// dtype, and `Error::Extension` or a typed backend source when LU
120    /// execution or pivot storage fails.
121    fn lu(&mut self, input: &Tensor) -> tenferro_tensor::Result<Vec<Tensor>>;
122
123    #[doc(hidden)]
124    fn lu_factor(&mut self, _input: &Tensor) -> tenferro_tensor::Result<Vec<Tensor>> {
125        Err(tenferro_tensor::Error::unsupported(
126            "lu_factor",
127            format!(
128                "backend {} does not implement internal packed LU factorization",
129                std::any::type_name::<Self>()
130            ),
131        ))
132    }
133
134    /// Compute complete-pivot LU outputs `(P, L, U, Q, parity)`.
135    ///
136    /// The reconstruction convention is `A = P^T * L * U * Q`, equivalently
137    /// `P * A * Q^T = L * U`. `parity` is a scalar real tensor containing
138    /// `+1` or `-1`: `F32` for `F32`/`C32` inputs and `F64` for `F64`/`C64`
139    /// inputs.
140    ///
141    /// # Errors
142    ///
143    /// Returns `Error::Validation` for an invalid rank, square-shape
144    /// requirement, or dtype, and `Error::Extension` or a typed backend source
145    /// when complete-pivot factorization cannot be executed.
146    fn full_piv_lu(&mut self, input: &Tensor) -> tenferro_tensor::Result<Vec<Tensor>>;
147
148    /// Solve a linear system through the complete-pivot LU path.
149    ///
150    /// With `transpose_a = false`, this solves `A * x = b`. With
151    /// `transpose_a = true`, this solves `A^T * x = b`.
152    ///
153    /// # Errors
154    ///
155    /// Returns `Error::Validation` for incompatible coefficient/rhs shapes or
156    /// dtypes, `Error::Extension` with `ErrorKind::NumericalFailure` for a
157    /// singular system, or a typed backend source for provider failure.
158    fn full_piv_lu_solve(
159        &mut self,
160        a: &Tensor,
161        b: &Tensor,
162        transpose_a: bool,
163    ) -> tenferro_tensor::Result<Tensor>;
164
165    /// Compute public SVD outputs `(U, S, Vt)`.
166    ///
167    /// # Errors
168    ///
169    /// Returns `Error::Validation` for an unsupported rank or dtype and a
170    /// typed `Error::Extension` or backend source when the solver fails.
171    fn svd(&mut self, input: &Tensor) -> tenferro_tensor::Result<Vec<Tensor>>;
172
173    /// Compute public SVD outputs `(U, S, Vt)` with explicit options.
174    ///
175    /// `derivative_eps` is validated for API consistency, but concrete backend
176    /// execution does not perform AD. `gauge` controls optional singular-vector
177    /// post-processing.
178    ///
179    /// # Examples
180    ///
181    /// ```rust
182    /// use tenferro_cpu::{with_cpu_exec_session, CpuBackend};
183    /// use tenferro_linalg::{LinalgBackend, SvdGauge, SvdOptions};
184    /// use tenferro_tensor::{BackendSessionHost, Tensor};
185    ///
186    /// let input = Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 0.0, 0.0, 2.0])?;
187    /// let mut host = CpuBackend::new();
188    /// let outputs = host.with_backend_session(|session| {
189    ///     with_cpu_exec_session(session, |backend| {
190    ///         backend.svd_with_options(
191    ///             &input,
192    ///             SvdOptions::default().gauge(SvdGauge::CanonicalPivot),
193    ///         )
194    ///     })
195    ///     .expect("CpuBackend must expose a CpuExecSession")
196    /// })?;
197    /// assert_eq!(outputs[1].shape(), &[2]);
198    /// # Ok::<(), tenferro_tensor::Error>(())
199    /// ```
200    ///
201    /// # Errors
202    ///
203    /// Returns [`tenferro_tensor::Error::Validation`] containing
204    /// [`tenferro_tensor::ValidationError::InvalidArgument`] when
205    /// `derivative_eps` is non-finite or non-positive, or when canonical gauge
206    /// output metadata is malformed. It can return
207    /// [`tenferro_tensor::Error::Validation`] with
208    /// [`tenferro_tensor::ValidationError::RankMismatch`],
209    /// [`tenferro_tensor::ValidationError::ShapeMismatch`], or
210    /// [`tenferro_tensor::ValidationError::DTypeMismatch`] for the input
211    /// or generated outputs, [`tenferro_tensor::Error::Extension`] with the
212    /// typed `tenferro_linalg::Error::UnsupportedDType` or
213    /// `NonConvergence` source, [`tenferro_tensor::Error::BackendSource`] for
214    /// provider calls, and [`tenferro_tensor::Error::RuntimeState`] for
215    /// placement failures. A CPU provider that was not compiled is reported
216    /// as [`tenferro_tensor::ValidationError::InvalidArgument`] on the
217    /// provider configuration.
218    fn svd_with_options(
219        &mut self,
220        input: &Tensor,
221        options: SvdOptions,
222    ) -> tenferro_tensor::Result<Vec<Tensor>> {
223        validate_derivative_eps("svd_with_options", options.derivative_eps)?;
224        let mut outputs = self.svd(input)?;
225        apply_svd_gauge(options.gauge, &mut outputs)?;
226        Ok(outputs)
227    }
228
229    /// Compute public full-matrices SVD outputs `(U, S, Vt)` with `U` shaped
230    /// `m x m` and `Vt` shaped `n x n`, so the trailing `Vt` rows span the
231    /// input's right nullspace.
232    ///
233    /// # Errors
234    ///
235    /// The default implementation returns `Error::Unsupported`: a backend that
236    /// does not implement the full variant reports it explicitly rather than
237    /// silently falling back to the thin decomposition. Implementing backends
238    /// may additionally return `Error::Validation` for an unsupported rank or
239    /// dtype and a typed backend source when the solver fails.
240    fn svd_full(&mut self, _input: &Tensor) -> tenferro_tensor::Result<Vec<Tensor>> {
241        Err(tenferro_tensor::Error::unsupported(
242            "svd_full",
243            format!(
244                "backend {} does not implement full-matrices SVD",
245                std::any::type_name::<Self>()
246            ),
247        ))
248    }
249
250    #[doc(hidden)]
251    fn svd_values(&mut self, _input: &Tensor) -> tenferro_tensor::Result<Tensor> {
252        Err(tenferro_tensor::Error::unsupported(
253            "svd_values",
254            format!(
255                "backend {} does not implement internal singular-values-only decomposition",
256                std::any::type_name::<Self>()
257            ),
258        ))
259    }
260
261    /// Compute a singular value decomposition from a tensor read target.
262    ///
263    /// Backends may canonicalize the input inside the same placement family, but
264    /// must not silently transfer between CPU and GPU memory.
265    ///
266    /// # Examples
267    ///
268    /// ```rust
269    /// use tenferro_linalg::LinalgBackend;
270    /// use tenferro_cpu::{with_cpu_exec_session, CpuBackend};
271    /// use tenferro_tensor::{BackendSessionHost, TensorRead, TensorView, TypedTensor};
272    ///
273    /// let input = TypedTensor::<f64>::from_vec_col_major(
274    ///     vec![2, 2],
275    ///     vec![1.0, 0.0, 0.0, 2.0],
276    /// )?;
277    /// let mut host = CpuBackend::new();
278    /// let outputs = host.with_backend_session(|session| {
279    ///     with_cpu_exec_session(session, |backend| {
280    ///         backend.svd_read(TensorRead::from_view(TensorView::F64(input.as_view())))
281    ///     })
282    ///     .expect("CpuBackend must expose a CpuExecSession")
283    /// })?;
284    /// assert_eq!(outputs[1].shape(), &[2]);
285    /// # Ok::<(), tenferro_tensor::Error>(())
286    /// ```
287    ///
288    /// # Errors
289    ///
290    /// The default implementation returns `Error::Unsupported` because the
291    /// backend does not accept tensor read targets; an implementation may instead
292    /// return validation or typed backend-source errors after canonicalizing
293    /// the view.
294    fn svd_read(&mut self, _input: TensorRead<'_>) -> tenferro_tensor::Result<Vec<Tensor>> {
295        Err(tenferro_tensor::Error::unsupported(
296            "svd",
297            "backend does not accept tensor reads at this execution boundary",
298        ))
299    }
300
301    #[doc(hidden)]
302    fn svd_values_read(&mut self, _input: TensorRead<'_>) -> tenferro_tensor::Result<Tensor> {
303        Err(tenferro_tensor::Error::unsupported(
304            "svd_values",
305            "backend does not implement borrowed singular-values-only decomposition",
306        ))
307    }
308
309    /// Compute public QR outputs `(Q, R)`.
310    ///
311    /// QR is thin: for an `m x n` input, `Q` has shape `m x min(m, n)` and
312    /// `R` has shape `min(m, n) x n`.
313    ///
314    /// # Errors
315    ///
316    /// Returns `Error::Validation` for an unsupported rank, shape, or dtype,
317    /// and a typed `Error::Extension` or backend source when QR execution
318    /// fails.
319    fn qr(&mut self, input: &Tensor) -> tenferro_tensor::Result<Vec<Tensor>>;
320
321    /// Compute public QR outputs `(Q, R)` with explicit options.
322    ///
323    /// `gauge` controls optional sign or phase post-processing.
324    ///
325    /// # Examples
326    ///
327    /// ```rust
328    /// use tenferro_cpu::{with_cpu_exec_session, CpuBackend};
329    /// use tenferro_linalg::{LinalgBackend, QrGauge, QrOptions};
330    /// use tenferro_tensor::{BackendSessionHost, Tensor};
331    ///
332    /// let input = Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 0.0, 0.0, 2.0])?;
333    /// let mut host = CpuBackend::new();
334    /// let outputs = host.with_backend_session(|session| {
335    ///     with_cpu_exec_session(session, |backend| {
336    ///         backend.qr_with_options(
337    ///             &input,
338    ///             QrOptions::default().gauge(QrGauge::PositiveDiagonal),
339    ///         )
340    ///     })
341    ///     .expect("CpuBackend must expose a CpuExecSession")
342    /// })?;
343    /// assert_eq!(outputs[0].shape(), &[2, 2]);
344    /// # Ok::<(), tenferro_tensor::Error>(())
345    /// ```
346    ///
347    /// # Errors
348    ///
349    /// Returns [`tenferro_tensor::Error::Validation`] containing
350    /// [`tenferro_tensor::ValidationError::RankMismatch`] or
351    /// [`tenferro_tensor::ValidationError::ShapeMismatch`] for an invalid
352    /// matrix input, or [`tenferro_tensor::ValidationError::InvalidArgument`]
353    /// for malformed gauge output metadata, checked size arithmetic, or an
354    /// unavailable compiled provider. A mismatched generated `Q`/`R` dtype is reported as
355    /// [`tenferro_tensor::ValidationError::DTypeMismatch`]. Provider
356    /// unsupported dtype or numerical rejection is
357    /// [`tenferro_tensor::Error::Extension`] with a typed linalg source, while
358    /// provider failures use [`tenferro_tensor::Error::BackendSource`] and a
359    /// backend-resident input uses [`tenferro_tensor::Error::RuntimeState`].
360    fn qr_with_options(
361        &mut self,
362        input: &Tensor,
363        options: QrOptions,
364    ) -> tenferro_tensor::Result<Vec<Tensor>> {
365        let mut outputs = self.qr(input)?;
366        apply_qr_gauge(options.gauge, &mut outputs)?;
367        Ok(outputs)
368    }
369
370    /// Compute public QR outputs `(Q, R)` from a tensor read target.
371    ///
372    /// Backends may canonicalize the input inside the same placement family, but
373    /// must not silently transfer between CPU and GPU memory.
374    ///
375    /// # Examples
376    ///
377    /// ```rust
378    /// use tenferro_linalg::LinalgBackend;
379    /// use tenferro_cpu::{with_cpu_exec_session, CpuBackend};
380    /// use tenferro_tensor::{BackendSessionHost, TensorRead, TensorView, TypedTensor};
381    ///
382    /// let input = TypedTensor::<f64>::from_vec_col_major(
383    ///     vec![2, 2],
384    ///     vec![1.0, 0.0, 0.0, 2.0],
385    /// )?;
386    /// let mut host = CpuBackend::new();
387    /// let outputs = host.with_backend_session(|session| {
388    ///     with_cpu_exec_session(session, |backend| {
389    ///         backend.qr_read(TensorRead::from_view(TensorView::F64(input.as_view())))
390    ///     })
391    ///     .expect("CpuBackend must expose a CpuExecSession")
392    /// })?;
393    /// assert_eq!(outputs[0].shape(), &[2, 2]);
394    /// assert_eq!(outputs[1].shape(), &[2, 2]);
395    /// # Ok::<(), tenferro_tensor::Error>(())
396    /// ```
397    ///
398    /// # Errors
399    ///
400    /// The default implementation returns `Error::Unsupported` because the
401    /// backend does not accept tensor read targets; implementations may return
402    /// validation or typed backend-source errors.
403    fn qr_read(&mut self, _input: TensorRead<'_>) -> tenferro_tensor::Result<Vec<Tensor>> {
404        Err(tenferro_tensor::Error::unsupported(
405            "qr",
406            "backend does not accept tensor reads at this execution boundary",
407        ))
408    }
409
410    /// Compute public Hermitian eigendecomposition outputs `(values, vectors)`.
411    ///
412    /// The returned vector order is `[values, vectors]`, where `values` has
413    /// shape `[n]` and `vectors` has shape `[n, n]`.
414    ///
415    /// # Errors
416    ///
417    /// Returns `Error::Validation` for a non-square or unsupported-dtype input
418    /// and a typed `Error::Extension` or backend source when eigendecomposition
419    /// fails.
420    fn eigh(&mut self, input: &Tensor) -> tenferro_tensor::Result<Vec<Tensor>>;
421
422    /// Compute public Hermitian eigendecomposition outputs with explicit options.
423    ///
424    /// `derivative_eps` is validated for API consistency, but concrete backend
425    /// execution does not perform AD. `gauge` controls optional eigenvector
426    /// post-processing.
427    ///
428    /// # Examples
429    ///
430    /// ```rust
431    /// use tenferro_cpu::{with_cpu_exec_session, CpuBackend};
432    /// use tenferro_linalg::{EighGauge, EighOptions, LinalgBackend};
433    /// use tenferro_tensor::{BackendSessionHost, Tensor};
434    ///
435    /// let input = Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 0.0, 0.0, 2.0])?;
436    /// let mut host = CpuBackend::new();
437    /// let outputs = host.with_backend_session(|session| {
438    ///     with_cpu_exec_session(session, |backend| {
439    ///         backend.eigh_with_options(
440    ///             &input,
441    ///             EighOptions::default()
442    ///                 .gauge(EighGauge::CanonicalPivot)
443    ///                 .derivative_eps(1.0e-10),
444    ///         )
445    ///     })
446    ///     .expect("CpuBackend must expose a CpuExecSession")
447    /// })?;
448    /// assert_eq!(outputs[0].shape(), &[2]);
449    /// # Ok::<(), tenferro_tensor::Error>(())
450    /// ```
451    ///
452    /// # Errors
453    ///
454    /// Returns [`tenferro_tensor::Error::Validation`] containing
455    /// [`tenferro_tensor::ValidationError::InvalidArgument`] when
456    /// `derivative_eps` is non-finite or non-positive, when canonical gauge
457    /// output metadata is malformed, or when checked output-size arithmetic
458    /// overflows. It can return [`tenferro_tensor::Error::Validation`] with
459    /// [`tenferro_tensor::ValidationError::RankMismatch`] or
460    /// [`tenferro_tensor::ValidationError::ShapeMismatch`] for the
461    /// matrix input, or [`tenferro_tensor::ValidationError::DTypeMismatch`]
462    /// for generated outputs. It can also return
463    /// [`tenferro_tensor::Error::Extension`] with typed
464    /// `tenferro_linalg::Error::UnsupportedDType` or `NonConvergence`, and
465    /// [`tenferro_tensor::Error::BackendSource`] or
466    /// [`tenferro_tensor::Error::RuntimeState`] for provider and placement
467    /// failures.
468    fn eigh_with_options(
469        &mut self,
470        input: &Tensor,
471        options: EighOptions,
472    ) -> tenferro_tensor::Result<Vec<Tensor>> {
473        validate_derivative_eps("eigh_with_options", options.derivative_eps)?;
474        let mut outputs = self.eigh(input)?;
475        apply_eigh_gauge(options.gauge, &mut outputs)?;
476        Ok(outputs)
477    }
478
479    /// Compute public Hermitian eigendecomposition outputs from a tensor read target.
480    ///
481    /// Backends may canonicalize the input inside the same placement family, but
482    /// must not silently transfer between CPU and GPU memory.
483    ///
484    /// # Examples
485    ///
486    /// ```rust
487    /// use tenferro_linalg::LinalgBackend;
488    /// use tenferro_cpu::{with_cpu_exec_session, CpuBackend};
489    /// use tenferro_tensor::{BackendSessionHost, TensorRead, TensorView, TypedTensor};
490    ///
491    /// let input = TypedTensor::<f64>::from_vec_col_major(
492    ///     vec![2, 2],
493    ///     vec![1.0, 0.0, 0.0, 2.0],
494    /// )?;
495    /// let mut host = CpuBackend::new();
496    /// let outputs = host.with_backend_session(|session| {
497    ///     with_cpu_exec_session(session, |backend| {
498    ///         backend.eigh_read(TensorRead::from_view(TensorView::F64(input.as_view())))
499    ///     })
500    ///     .expect("CpuBackend must expose a CpuExecSession")
501    /// })?;
502    /// assert_eq!(outputs[0].shape(), &[2]);
503    /// assert_eq!(outputs[1].shape(), &[2, 2]);
504    /// # Ok::<(), tenferro_tensor::Error>(())
505    /// ```
506    ///
507    /// # Errors
508    ///
509    /// The default implementation returns `Error::Unsupported` because the
510    /// backend does not accept tensor read targets; implementations may return
511    /// validation or typed backend-source errors.
512    fn eigh_read(&mut self, _input: TensorRead<'_>) -> tenferro_tensor::Result<Vec<Tensor>> {
513        Err(tenferro_tensor::Error::unsupported(
514            "eigh",
515            "backend does not accept tensor reads at this execution boundary",
516        ))
517    }
518
519    /// Compute Cholesky factorization from a tensor read target.
520    ///
521    /// Backends may canonicalize the input inside the same placement family, but
522    /// must not silently transfer between CPU and GPU memory.
523    ///
524    /// # Examples
525    ///
526    /// ```rust
527    /// use tenferro_linalg::LinalgBackend;
528    /// use tenferro_cpu::{with_cpu_exec_session, CpuBackend};
529    /// use tenferro_tensor::{BackendSessionHost, TensorRead, TensorView, TypedTensor};
530    ///
531    /// let input = TypedTensor::<f64>::from_vec_col_major(
532    ///     vec![2, 2],
533    ///     vec![4.0, 2.0, 2.0, 3.0],
534    /// )?;
535    /// let mut host = CpuBackend::new();
536    /// let output = host.with_backend_session(|session| {
537    ///     with_cpu_exec_session(session, |backend| {
538    ///         backend.cholesky_read(TensorRead::from_view(TensorView::F64(input.as_view())))
539    ///     })
540    ///     .expect("CpuBackend must expose a CpuExecSession")
541    /// })?;
542    /// assert_eq!(output.shape(), &[2, 2]);
543    /// # Ok::<(), tenferro_tensor::Error>(())
544    /// ```
545    ///
546    /// # Errors
547    ///
548    /// The default implementation returns `Error::Unsupported` because the
549    /// backend does not accept tensor read targets; implementations may return
550    /// validation or typed backend-source errors.
551    fn cholesky_read(&mut self, _input: TensorRead<'_>) -> tenferro_tensor::Result<Tensor> {
552        Err(tenferro_tensor::Error::unsupported(
553            "cholesky",
554            "backend does not accept tensor reads at this execution boundary",
555        ))
556    }
557
558    /// Compute public LU outputs from a tensor read target.
559    ///
560    /// Backends may canonicalize the input inside the same placement family, but
561    /// must not silently transfer between CPU and GPU memory.
562    ///
563    /// # Examples
564    ///
565    /// ```rust
566    /// use tenferro_linalg::LinalgBackend;
567    /// use tenferro_cpu::{with_cpu_exec_session, CpuBackend};
568    /// use tenferro_tensor::{BackendSessionHost, TensorRead, TensorView, TypedTensor};
569    ///
570    /// let input = TypedTensor::<f64>::from_vec_col_major(
571    ///     vec![2, 2],
572    ///     vec![1.0, 3.0, 2.0, 4.0],
573    /// )?;
574    /// let mut host = CpuBackend::new();
575    /// let outputs = host.with_backend_session(|session| {
576    ///     with_cpu_exec_session(session, |backend| {
577    ///         backend.lu_read(TensorRead::from_view(TensorView::F64(input.as_view())))
578    ///     })
579    ///     .expect("CpuBackend must expose a CpuExecSession")
580    /// })?;
581    /// assert_eq!(outputs.len(), 4);
582    /// # Ok::<(), tenferro_tensor::Error>(())
583    /// ```
584    ///
585    /// # Errors
586    ///
587    /// The default implementation returns `Error::Unsupported` because the
588    /// backend does not accept tensor read targets; implementations may return
589    /// validation or typed backend-source errors.
590    fn lu_read(&mut self, _input: TensorRead<'_>) -> tenferro_tensor::Result<Vec<Tensor>> {
591        Err(tenferro_tensor::Error::unsupported(
592            "lu",
593            "backend does not accept tensor reads at this execution boundary",
594        ))
595    }
596
597    /// Compute public full-pivoting LU outputs from a tensor read target.
598    ///
599    /// Backends may canonicalize the input inside the same placement family, but
600    /// must not silently transfer between CPU and GPU memory.
601    ///
602    /// # Examples
603    ///
604    /// ```rust
605    /// use tenferro_linalg::LinalgBackend;
606    /// use tenferro_cpu::{with_cpu_exec_session, CpuBackend};
607    /// use tenferro_tensor::{BackendSessionHost, TensorRead, TensorView, TypedTensor};
608    ///
609    /// let input = TypedTensor::<f64>::from_vec_col_major(
610    ///     vec![2, 2],
611    ///     vec![1.0, 3.0, 2.0, 4.0],
612    /// )?;
613    /// let mut host = CpuBackend::new();
614    /// let outputs = host.with_backend_session(|session| {
615    ///     with_cpu_exec_session(session, |backend| {
616    ///         backend.full_piv_lu_read(TensorRead::from_view(TensorView::F64(input.as_view())))
617    ///     })
618    ///     .expect("CpuBackend must expose a CpuExecSession")
619    /// })?;
620    /// assert_eq!(outputs.len(), 5);
621    /// # Ok::<(), tenferro_tensor::Error>(())
622    /// ```
623    ///
624    /// # Errors
625    ///
626    /// The default implementation returns `Error::Unsupported` because the
627    /// backend does not accept tensor read targets; implementations may return
628    /// validation or typed backend-source errors.
629    fn full_piv_lu_read(&mut self, _input: TensorRead<'_>) -> tenferro_tensor::Result<Vec<Tensor>> {
630        Err(tenferro_tensor::Error::unsupported(
631            "full_piv_lu",
632            "backend does not accept tensor reads at this execution boundary",
633        ))
634    }
635
636    /// Compute general eigendecomposition outputs from a tensor read target.
637    ///
638    /// Backends may canonicalize the input inside the same placement family, but
639    /// must not silently transfer between CPU and GPU memory.
640    ///
641    /// # Examples
642    ///
643    /// ```rust
644    /// use tenferro_linalg::LinalgBackend;
645    /// use tenferro_cpu::{with_cpu_exec_session, CpuBackend};
646    /// use tenferro_tensor::{BackendSessionHost, TensorRead, TensorView, TypedTensor};
647    ///
648    /// let input = TypedTensor::<f64>::from_vec_col_major(
649    ///     vec![2, 2],
650    ///     vec![2.0, 0.0, 0.0, 3.0],
651    /// )?;
652    /// let mut host = CpuBackend::new();
653    /// let outputs = host.with_backend_session(|session| {
654    ///     with_cpu_exec_session(session, |backend| {
655    ///         backend.eig_read(TensorRead::from_view(TensorView::F64(input.as_view())))
656    ///     })
657    ///     .expect("CpuBackend must expose a CpuExecSession")
658    /// })?;
659    /// assert_eq!(outputs.len(), 2);
660    /// # Ok::<(), tenferro_tensor::Error>(())
661    /// ```
662    ///
663    /// # Errors
664    ///
665    /// The default implementation returns `Error::Unsupported` because the
666    /// backend does not accept tensor read targets; implementations may return
667    /// validation or typed backend-source errors.
668    fn eig_read(&mut self, _input: TensorRead<'_>) -> tenferro_tensor::Result<Vec<Tensor>> {
669        Err(tenferro_tensor::Error::unsupported(
670            "eig",
671            "backend does not accept tensor reads at this execution boundary",
672        ))
673    }
674
675    #[doc(hidden)]
676    fn eigh_values(&mut self, _input: &Tensor) -> tenferro_tensor::Result<Tensor> {
677        Err(tenferro_tensor::Error::unsupported(
678            "eigh_values",
679            format!(
680                "backend {} does not implement internal Hermitian eigenvalues-only decomposition",
681                std::any::type_name::<Self>()
682            ),
683        ))
684    }
685
686    #[doc(hidden)]
687    fn eigh_values_read(&mut self, _input: TensorRead<'_>) -> tenferro_tensor::Result<Tensor> {
688        Err(tenferro_tensor::Error::unsupported(
689            "eigh_values",
690            "backend does not implement borrowed Hermitian eigenvalues-only decomposition",
691        ))
692    }
693
694    /// Compute public general eigendecomposition outputs `(values, vectors)`.
695    ///
696    /// # Errors
697    ///
698    /// Returns `Error::Validation` for a non-square, rank, or dtype mismatch,
699    /// and a typed `Error::Extension` or backend source when the eigensolver
700    /// fails.
701    fn eig(&mut self, input: &Tensor) -> tenferro_tensor::Result<Vec<Tensor>>;
702
703    #[doc(hidden)]
704    fn eig_values(&mut self, _input: &Tensor) -> tenferro_tensor::Result<Tensor> {
705        Err(tenferro_tensor::Error::unsupported(
706            "eig_values",
707            format!(
708                "backend {} does not implement internal general eigenvalues-only decomposition",
709                std::any::type_name::<Self>()
710            ),
711        ))
712    }
713
714    /// Solve a dense linear system.
715    ///
716    /// # Errors
717    ///
718    /// Returns `Error::Validation` for incompatible matrix/rhs shapes, rank,
719    /// or dtype; `Error::Extension` with `ErrorKind::NumericalFailure` for a
720    /// singular system; or a typed backend source for provider failure.
721    fn solve(&mut self, a: &Tensor, b: &Tensor) -> tenferro_tensor::Result<Tensor>;
722
723    /// Solve a linear system from tensor read targets.
724    ///
725    /// Backends may canonicalize the inputs inside the same placement family,
726    /// but must not silently transfer between CPU and GPU memory.
727    ///
728    /// # Examples
729    ///
730    /// ```rust
731    /// use tenferro_cpu::{with_cpu_exec_session, CpuBackend};
732    /// use tenferro_linalg::LinalgBackend;
733    /// use tenferro_tensor::{BackendSessionHost, Tensor, TensorRead};
734    ///
735    /// let a = Tensor::from_vec_col_major(vec![2, 2], vec![2.0_f64, 0.0, 0.0, 3.0])?;
736    /// let b = Tensor::from_vec_col_major(vec![2, 1], vec![4.0_f64, 9.0])?;
737    /// let mut host = CpuBackend::new();
738    /// let x = host.with_backend_session(|session| {
739    ///     with_cpu_exec_session(session, |backend| {
740    ///         backend.solve_read(
741    ///             TensorRead::from_tensor(&a),
742    ///             TensorRead::from_tensor(&b),
743    ///         )
744    ///     })
745    ///     .expect("CpuBackend must expose a CpuExecSession")
746    /// })?;
747    /// let Tensor::F64(x) = x else { unreachable!("F64 inputs return F64 output") };
748    /// assert_eq!(x.host_data()?, &[2.0, 3.0]);
749    /// # Ok::<(), tenferro_tensor::Error>(())
750    /// ```
751    ///
752    /// # Errors
753    ///
754    /// The default implementation returns `Error::Unsupported` because the
755    /// backend does not accept tensor read targets. Implementations may return
756    /// `Error::Validation` for incompatible shapes or dtypes,
757    /// `Error::RuntimeState` for invalid placement, `Error::Extension` for a
758    /// singular system, or a typed backend-source error.
759    fn solve_read(
760        &mut self,
761        _a: TensorRead<'_>,
762        _b: TensorRead<'_>,
763    ) -> tenferro_tensor::Result<Tensor> {
764        Err(tenferro_tensor::Error::unsupported(
765            "solve",
766            "backend does not accept tensor reads at this execution boundary",
767        ))
768    }
769
770    /// Solve into a caller-owned destination.
771    ///
772    /// The default preserves the ordinary read path and copies its result into
773    /// `out`. Backends with a native destination path may override this method,
774    /// but must validate the destination before the first write and preserve
775    /// the same shape, dtype, placement, aliasing, and error contracts.
776    ///
777    /// # Errors
778    ///
779    /// Returns `tenferro_tensor_core::ShapeMismatch` or
780    /// `tenferro_tensor_core::ValidationError::DTypeMismatch` for incompatible
781    /// destination metadata, `tenferro_tensor_core::ValidationError::InvalidArgument`
782    /// for aliasing or placement violations, `Error::Unsupported` when the
783    /// provider is unavailable, and `Error::Singular` for a singular system.
784    ///
785    /// # Examples
786    ///
787    /// ```rust
788    /// use tenferro_cpu::{with_cpu_exec_session, CpuBackend};
789    /// use tenferro_linalg::LinalgBackend;
790    /// use tenferro_tensor::{BackendSessionHost, Tensor, TensorRead, TensorWrite};
791    ///
792    /// let a = Tensor::from_vec_col_major(vec![2, 2], vec![2.0_f64, 0.0, 0.0, 4.0])?;
793    /// let b = Tensor::from_vec_col_major(vec![2, 1], vec![4.0_f64, 8.0])?;
794    /// let mut out = Tensor::from_vec_col_major(vec![2, 1], vec![0.0_f64; 2])?;
795    /// let mut host = CpuBackend::new();
796    /// host.with_backend_session(|session| {
797    ///     with_cpu_exec_session(session, |backend| {
798    ///         backend.solve_read_into(
799    ///             TensorRead::from_tensor(&a),
800    ///             TensorRead::from_tensor(&b),
801    ///             TensorWrite::from_tensor(&mut out),
802    ///         )
803    ///     })
804    ///     .expect("CpuBackend must expose a CpuExecSession")
805    /// })?;
806    /// assert_eq!(out.as_slice::<f64>()?, &[2.0, 2.0]);
807    /// # Ok::<(), tenferro_tensor::Error>(())
808    /// ```
809    fn solve_read_into(
810        &mut self,
811        a: TensorRead<'_>,
812        b: TensorRead<'_>,
813        out: TensorWrite<'_>,
814    ) -> tenferro_tensor::Result<()> {
815        solve_read_into_default(self, a, b, out)
816    }
817
818    #[doc(hidden)]
819    fn lu_solve_prepared(
820        &mut self,
821        _a: &Tensor,
822        _packed_lu: &Tensor,
823        _pivots: &Tensor,
824        _b: &Tensor,
825        _transpose_a: bool,
826        _conjugate_a: bool,
827    ) -> tenferro_tensor::Result<Tensor> {
828        Err(tenferro_tensor::Error::unsupported(
829            "lu_solve_prepared",
830            format!(
831                "backend {} does not implement internal prepared LU solve",
832                std::any::type_name::<Self>()
833            ),
834        ))
835    }
836}
837
838pub(crate) fn solve_read_into_default<B: LinalgBackend + ?Sized>(
839    backend: &mut B,
840    a: TensorRead<'_>,
841    b: TensorRead<'_>,
842    out: TensorWrite<'_>,
843) -> tenferro_tensor::Result<()> {
844    validate_solve_read_into(&a, &b, &out)?;
845    let result = backend.solve_read(a, b)?;
846    backend.copy_read_into(TensorRead::from_tensor(&result), out)
847}
848
849pub(crate) fn validate_solve_read_into(
850    _a: &TensorRead<'_>,
851    b: &TensorRead<'_>,
852    out: &TensorWrite<'_>,
853) -> tenferro_tensor::Result<()> {
854    if b.shape() != out.shape() {
855        return Err(tenferro_tensor::Error::shape_mismatch(
856            "solve_read_into",
857            b.shape().to_vec(),
858            out.shape().to_vec(),
859        ));
860    }
861    if b.dtype() != out.dtype() {
862        return Err(tenferro_tensor::Error::dtype_mismatch(
863            "solve_read_into",
864            b.dtype(),
865            out.dtype(),
866        ));
867    }
868    if b.placement() != out.as_read().placement() {
869        return Err(tenferro_tensor::Error::invalid_argument(
870            "solve_read_into",
871            "out",
872            format!(
873                "destination placement {:?} does not match rhs placement {:?}",
874                out.as_read().placement(),
875                b.placement()
876            ),
877        ));
878    }
879    let inputs = [_a.clone(), b.clone()];
880    tenferro_tensor::backend::validate_read_into_destination("solve_read_into", &inputs, out)
881}