Skip to main content

tenferro_linalg/
backend.rs

1use std::ops::Range;
2
3use tenferro_tensor::{BackendSession, Tensor, TensorRead, TensorWrite};
4
5/// Compact provider-neutral Householder QR payload used by backend hooks.
6#[doc(hidden)]
7#[derive(Debug)]
8pub struct CompactQrResult {
9    pub(crate) packed: Tensor,
10    pub(crate) coeff: Tensor,
11}
12
13pub(crate) use crate::error::unsupported_dtype;
14use crate::extension::{
15    apply_eigh_gauge, apply_qr_gauge, apply_svd_gauge, validate_derivative_eps, EighDriver,
16    EighOptions, QrOptions, SvdDriver, SvdOptions,
17};
18use crate::RankRevealingQrOptions;
19
20/// Backend surface required by the linalg extension runtime.
21///
22/// # Examples
23///
24/// ```rust
25/// use tenferro_cpu::{with_cpu_exec_session, CpuBackend, CpuExecSession};
26/// use tenferro_linalg::backend::LinalgBackend;
27/// use tenferro_tensor::BackendSessionHost;
28///
29/// fn assert_linalg_backend<B: LinalgBackend>() {}
30///
31/// assert_linalg_backend::<CpuExecSession<'static>>();
32/// let mut host = CpuBackend::new();
33/// host.with_backend_session(|session| {
34///     with_cpu_exec_session(session, |_backend| ())
35///         .expect("CpuBackend must expose a CpuExecSession");
36/// })?;
37/// # Ok::<(), Box<dyn std::error::Error>>(())
38/// ```
39pub trait LinalgBackend: BackendSession {
40    /// Compute a Cholesky factorization.
41    ///
42    /// # Errors
43    ///
44    /// Returns `Error::Validation` for non-matrix, non-square, or unsupported
45    /// input dtypes; `Error::Extension` with `ErrorKind::NumericalFailure`
46    /// when the matrix is not positive definite; or a typed backend source
47    /// when the provider cannot execute the factorization.
48    fn cholesky(&mut self, input: &Tensor) -> tenferro_tensor::Result<Tensor>;
49
50    /// Solve a triangular linear system with explicit side, triangle,
51    /// transpose, and unit-diagonal flags.
52    ///
53    /// # Errors
54    ///
55    /// Returns `Error::Validation` for incompatible matrix/rhs shapes, rank,
56    /// or dtype; `Error::Extension` with `ErrorKind::NumericalFailure` for a
57    /// singular or zero-diagonal system; or a typed backend source for a
58    /// provider failure.
59    fn triangular_solve(
60        &mut self,
61        a: &Tensor,
62        b: &Tensor,
63        left_side: bool,
64        lower: bool,
65        transpose_a: bool,
66        unit_diagonal: bool,
67    ) -> tenferro_tensor::Result<Tensor>;
68
69    // INVARIANT: the six flags are the public triangular-solve contract and mirror the owned hook.
70    #[allow(clippy::too_many_arguments)]
71    /// Solve a triangular linear system from tensor read targets.
72    ///
73    /// Backends may canonicalize the inputs inside the same placement family,
74    /// but must not silently transfer between CPU and GPU memory.
75    ///
76    /// # Examples
77    ///
78    /// ```rust
79    /// use tenferro_cpu::{with_cpu_exec_session, CpuBackend};
80    /// use tenferro_linalg::LinalgBackend;
81    /// use tenferro_tensor::{BackendSessionHost, Tensor, TensorRead};
82    ///
83    /// let a = Tensor::from_vec_col_major(vec![2, 2], vec![2.0_f64, 0.0, 1.0, 3.0])?;
84    /// let b = Tensor::from_vec_col_major(vec![2, 1], vec![4.0_f64, 9.0])?;
85    /// let mut host = CpuBackend::new();
86    /// let x = host.with_backend_session(|session| {
87    ///     with_cpu_exec_session(session, |backend| {
88    ///         backend.triangular_solve_read(
89    ///             TensorRead::from_tensor(&a),
90    ///             TensorRead::from_tensor(&b),
91    ///             true,
92    ///             false,
93    ///             false,
94    ///             false,
95    ///         )
96    ///     })
97    ///     .expect("CpuBackend must expose a CpuExecSession")
98    /// })??;
99    /// let x = x.into_typed::<f64>().expect("F64 inputs return F64 output");
100    /// assert_eq!(x.host_data()?, &[0.5, 3.0]);
101    /// # Ok::<(), tenferro_tensor::Error>(())
102    /// ```
103    ///
104    /// # Errors
105    ///
106    /// The default implementation returns `Error::Unsupported` because the
107    /// backend does not accept tensor read targets. Implementations may return
108    /// `Error::Validation` for incompatible shapes or dtypes,
109    /// `Error::RuntimeState` for invalid placement, `Error::Extension` for a
110    /// singular system, or a typed backend-source error.
111    fn triangular_solve_read(
112        &mut self,
113        _a: TensorRead<'_>,
114        _b: TensorRead<'_>,
115        _left_side: bool,
116        _lower: bool,
117        _transpose_a: bool,
118        _unit_diagonal: bool,
119    ) -> tenferro_tensor::Result<Tensor> {
120        Err(tenferro_tensor::Error::unsupported(
121            "triangular_solve",
122            "backend does not accept tensor reads at this execution boundary",
123        ))
124    }
125
126    /// Compute public LU outputs `(P, L, U, parity)`.
127    ///
128    /// # Errors
129    ///
130    /// Returns `Error::Validation` when the input is not a supported matrix or
131    /// dtype, and `Error::Extension` or a typed backend source when LU
132    /// execution or pivot storage fails.
133    fn lu(&mut self, input: &Tensor) -> tenferro_tensor::Result<Vec<Tensor>>;
134
135    #[doc(hidden)]
136    fn lu_factor(&mut self, _input: &Tensor) -> tenferro_tensor::Result<Vec<Tensor>> {
137        Err(tenferro_tensor::Error::unsupported(
138            "lu_factor",
139            format!(
140                "backend {} does not implement internal packed LU factorization",
141                std::any::type_name::<Self>()
142            ),
143        ))
144    }
145
146    /// Compute complete-pivot LU outputs `(P, L, U, Q, parity)`.
147    ///
148    /// The reconstruction convention is `A = P^T * L * U * Q`, equivalently
149    /// `P * A * Q^T = L * U`. `parity` is a scalar real tensor containing
150    /// `+1` or `-1`: `F32` for `F32`/`C32` inputs and `F64` for `F64`/`C64`
151    /// inputs.
152    ///
153    /// # Errors
154    ///
155    /// Returns `Error::Validation` for an invalid rank, square-shape
156    /// requirement, or dtype, and `Error::Extension` or a typed backend source
157    /// when complete-pivot factorization cannot be executed.
158    fn full_piv_lu(&mut self, input: &Tensor) -> tenferro_tensor::Result<Vec<Tensor>>;
159
160    /// Solve a linear system through the complete-pivot LU path.
161    ///
162    /// With `transpose_a = false`, this solves `A * x = b`. With
163    /// `transpose_a = true`, this solves `A^T * x = b`.
164    ///
165    /// # Errors
166    ///
167    /// Returns `Error::Validation` for incompatible coefficient/rhs shapes or
168    /// dtypes, `Error::Extension` with `ErrorKind::NumericalFailure` for a
169    /// singular system, or a typed backend source for provider failure.
170    fn full_piv_lu_solve(
171        &mut self,
172        a: &Tensor,
173        b: &Tensor,
174        transpose_a: bool,
175    ) -> tenferro_tensor::Result<Tensor>;
176
177    /// Compute public SVD outputs `(U, S, Vt)`.
178    ///
179    /// # Errors
180    ///
181    /// Returns `Error::Validation` for an unsupported rank or dtype and a
182    /// typed `Error::Extension` or backend source when the solver fails.
183    fn svd(&mut self, input: &Tensor) -> tenferro_tensor::Result<Vec<Tensor>>;
184
185    /// Compute public SVD outputs `(U, S, Vt)` with explicit options.
186    ///
187    /// `derivative_eps` is validated for API consistency, but concrete backend
188    /// execution does not perform AD. `gauge` controls optional singular-vector
189    /// post-processing. `driver` selects the cuSOLVER routine on the CUDA
190    /// backend; the default implementation ignores it because CPU providers
191    /// have a single SVD kernel.
192    ///
193    /// # Examples
194    ///
195    /// ```rust
196    /// use tenferro_cpu::{with_cpu_exec_session, CpuBackend};
197    /// use tenferro_linalg::{LinalgBackend, SvdGauge, SvdOptions};
198    /// use tenferro_tensor::{BackendSessionHost, Tensor};
199    ///
200    /// let input = Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 0.0, 0.0, 2.0])?;
201    /// let mut host = CpuBackend::new();
202    /// let outputs = host.with_backend_session(|session| {
203    ///     with_cpu_exec_session(session, |backend| {
204    ///         backend.svd_with_options(
205    ///             &input,
206    ///             SvdOptions::default().gauge(SvdGauge::CanonicalPivot),
207    ///         )
208    ///     })
209    ///     .expect("CpuBackend must expose a CpuExecSession")
210    /// })??;
211    /// assert_eq!(outputs[1].shape(), &[2]);
212    /// # Ok::<(), tenferro_tensor::Error>(())
213    /// ```
214    ///
215    /// # Errors
216    ///
217    /// Returns [`tenferro_tensor::Error::Validation`] containing
218    /// [`tenferro_tensor::ValidationError::InvalidArgument`] when
219    /// `derivative_eps` is non-finite or non-positive, or when canonical gauge
220    /// output metadata is malformed. It can return
221    /// [`tenferro_tensor::Error::Validation`] with
222    /// [`tenferro_tensor::ValidationError::RankMismatch`],
223    /// [`tenferro_tensor::ValidationError::ShapeMismatch`], or
224    /// [`tenferro_tensor::ValidationError::DTypeMismatch`] for the input
225    /// or generated outputs, [`tenferro_tensor::Error::Extension`] with the
226    /// typed `tenferro_linalg::Error::UnsupportedDType` or
227    /// `NonConvergence` source, [`tenferro_tensor::Error::BackendSource`] for
228    /// provider calls, and [`tenferro_tensor::Error::RuntimeState`] for
229    /// placement failures. A CPU provider that was not compiled is reported
230    /// as [`tenferro_tensor::ValidationError::InvalidArgument`] on the
231    /// provider configuration.
232    fn svd_with_options(
233        &mut self,
234        input: &Tensor,
235        options: SvdOptions,
236    ) -> tenferro_tensor::Result<Vec<Tensor>> {
237        validate_derivative_eps("svd_with_options", options.derivative_eps)?;
238        let mut outputs = self.svd(input)?;
239        apply_svd_gauge(options.gauge, &mut outputs)?;
240        Ok(outputs)
241    }
242
243    /// Compute public full-matrices SVD outputs `(U, S, Vt)` with `U` shaped
244    /// `m x m` and `Vt` shaped `n x n`, so the trailing `Vt` rows span the
245    /// input's right nullspace.
246    ///
247    /// # Errors
248    ///
249    /// The default implementation returns `Error::Unsupported`: a backend that
250    /// does not implement the full variant reports it explicitly rather than
251    /// silently falling back to the thin decomposition. Implementing backends
252    /// may additionally return `Error::Validation` for an unsupported rank or
253    /// dtype and a typed backend source when the solver fails.
254    fn svd_full(&mut self, _input: &Tensor) -> tenferro_tensor::Result<Vec<Tensor>> {
255        Err(tenferro_tensor::Error::unsupported(
256            "svd_full",
257            format!(
258                "backend {} does not implement full-matrices SVD",
259                std::any::type_name::<Self>()
260            ),
261        ))
262    }
263
264    /// Compute full-matrices SVD outputs `(U, S, Vt)` from a tensor read target.
265    ///
266    /// This is the borrowed-input counterpart of [`LinalgBackend::svd_full`] and
267    /// keeps the same output contract: `U` is `m x m`, `S` has `min(m, n)`
268    /// entries, and `Vt` is `n x n`. Backends may canonicalize the input inside
269    /// the same placement family, but must not silently transfer between CPU
270    /// and GPU memory, and must not substitute the thin decomposition.
271    ///
272    /// # Examples
273    ///
274    /// ```rust
275    /// use tenferro_cpu::{with_cpu_exec_session, CpuBackend};
276    /// use tenferro_linalg::LinalgBackend;
277    /// use tenferro_tensor::{BackendSessionHost, TensorRead, TensorView, TypedTensor};
278    ///
279    /// let input = TypedTensor::<f64>::from_vec_col_major(vec![1, 2], vec![1.0, 1.0])?;
280    /// let mut host = CpuBackend::new();
281    /// let outputs = host.with_backend_session(|session| {
282    ///     with_cpu_exec_session(session, |backend| {
283    ///         backend.svd_full_read(TensorRead::from_view(TensorView::F64(input.as_view())))
284    ///     })
285    ///     .expect("CpuBackend must expose a CpuExecSession")
286    /// })??;
287    /// assert_eq!(outputs[0].shape(), &[1, 1]);
288    /// assert_eq!(outputs[1].shape(), &[1]);
289    /// assert_eq!(outputs[2].shape(), &[2, 2]);
290    /// # Ok::<(), tenferro_tensor::Error>(())
291    /// ```
292    ///
293    /// # Errors
294    ///
295    /// The default implementation returns `Error::Unsupported` because the
296    /// backend does not accept tensor read targets for the full variant; an
297    /// implementation may instead return `Error::Validation` for an unsupported
298    /// rank or dtype, `Error::Unsupported` when the selected provider has no
299    /// full-matrices kernel, and a typed backend source when the solver fails.
300    fn svd_full_read(&mut self, _input: TensorRead<'_>) -> tenferro_tensor::Result<Vec<Tensor>> {
301        Err(tenferro_tensor::Error::unsupported(
302            "svd_full",
303            "backend does not accept tensor reads for full-matrices SVD at this execution boundary",
304        ))
305    }
306
307    #[doc(hidden)]
308    fn svd_values(&mut self, _input: &Tensor) -> tenferro_tensor::Result<Tensor> {
309        Err(tenferro_tensor::Error::unsupported(
310            "svd_values",
311            format!(
312                "backend {} does not implement internal singular-values-only decomposition",
313                std::any::type_name::<Self>()
314            ),
315        ))
316    }
317
318    /// Singular values only with an explicit CUDA driver. The default
319    /// implementation ignores the driver, matching [`LinalgBackend::svd_with_options`].
320    #[doc(hidden)]
321    fn svd_values_with_driver(
322        &mut self,
323        input: &Tensor,
324        _driver: SvdDriver,
325    ) -> tenferro_tensor::Result<Tensor> {
326        self.svd_values(input)
327    }
328
329    /// Compute a singular value decomposition from a tensor read target.
330    ///
331    /// Backends may canonicalize the input inside the same placement family, but
332    /// must not silently transfer between CPU and GPU memory.
333    ///
334    /// # Examples
335    ///
336    /// ```rust
337    /// use tenferro_linalg::LinalgBackend;
338    /// use tenferro_cpu::{with_cpu_exec_session, CpuBackend};
339    /// use tenferro_tensor::{BackendSessionHost, TensorRead, TensorView, TypedTensor};
340    ///
341    /// let input = TypedTensor::<f64>::from_vec_col_major(
342    ///     vec![2, 2],
343    ///     vec![1.0, 0.0, 0.0, 2.0],
344    /// )?;
345    /// let mut host = CpuBackend::new();
346    /// let outputs = host.with_backend_session(|session| {
347    ///     with_cpu_exec_session(session, |backend| {
348    ///         backend.svd_read(TensorRead::from_view(TensorView::F64(input.as_view())))
349    ///     })
350    ///     .expect("CpuBackend must expose a CpuExecSession")
351    /// })??;
352    /// assert_eq!(outputs[1].shape(), &[2]);
353    /// # Ok::<(), tenferro_tensor::Error>(())
354    /// ```
355    ///
356    /// # Errors
357    ///
358    /// The default implementation returns `Error::Unsupported` because the
359    /// backend does not accept tensor read targets; an implementation may instead
360    /// return validation or typed backend-source errors after canonicalizing
361    /// the view.
362    fn svd_read(&mut self, _input: TensorRead<'_>) -> tenferro_tensor::Result<Vec<Tensor>> {
363        Err(tenferro_tensor::Error::unsupported(
364            "svd",
365            "backend does not accept tensor reads at this execution boundary",
366        ))
367    }
368
369    /// Compute public SVD outputs `(U, S, Vt)` from a tensor read target with
370    /// explicit options.
371    ///
372    /// This is the borrowed-input counterpart of
373    /// [`LinalgBackend::svd_with_options`]. The default implementation runs
374    /// [`LinalgBackend::svd_read`] and applies the gauge on the host; it
375    /// ignores `driver` because CPU providers have a single SVD kernel. The
376    /// CUDA backend overrides it so the driver reaches cuSOLVER.
377    ///
378    /// # Examples
379    ///
380    /// ```rust
381    /// use tenferro_cpu::{with_cpu_exec_session, CpuBackend};
382    /// use tenferro_linalg::{LinalgBackend, SvdDriver, SvdOptions};
383    /// use tenferro_tensor::{BackendSessionHost, TensorRead, TensorView, TypedTensor};
384    ///
385    /// let input = TypedTensor::<f64>::from_vec_col_major(
386    ///     vec![2, 2],
387    ///     vec![1.0, 0.0, 0.0, 2.0],
388    /// )?;
389    /// let mut host = CpuBackend::new();
390    /// let outputs = host.with_backend_session(|session| {
391    ///     with_cpu_exec_session(session, |backend| {
392    ///         backend.svd_with_options_read(
393    ///             TensorRead::from_view(TensorView::F64(input.as_view())),
394    ///             SvdOptions::default().driver(SvdDriver::Gesvd),
395    ///         )
396    ///     })
397    ///     .expect("CpuBackend must expose a CpuExecSession")
398    /// })??;
399    /// assert_eq!(outputs[1].shape(), &[2]);
400    /// # Ok::<(), tenferro_tensor::Error>(())
401    /// ```
402    ///
403    /// # Errors
404    ///
405    /// Returns [`tenferro_tensor::Error::Validation`] containing
406    /// [`tenferro_tensor::ValidationError::InvalidArgument`] when
407    /// `derivative_eps` is non-finite or non-positive, plus the validation,
408    /// unsupported-dtype, numerical, placement, or typed backend/provider
409    /// errors from [`LinalgBackend::svd_read`] and the gauge metadata and
410    /// host-access errors from the default host gauge path.
411    fn svd_with_options_read(
412        &mut self,
413        input: TensorRead<'_>,
414        options: SvdOptions,
415    ) -> tenferro_tensor::Result<Vec<Tensor>> {
416        validate_derivative_eps("svd_with_options_read", options.derivative_eps)?;
417        let mut outputs = self.svd_read(input)?;
418        apply_svd_gauge(options.gauge, &mut outputs)?;
419        Ok(outputs)
420    }
421
422    #[doc(hidden)]
423    fn svd_values_read(&mut self, _input: TensorRead<'_>) -> tenferro_tensor::Result<Tensor> {
424        Err(tenferro_tensor::Error::unsupported(
425            "svd_values",
426            "backend does not implement borrowed singular-values-only decomposition",
427        ))
428    }
429
430    /// Borrowed-input counterpart of [`LinalgBackend::svd_values_with_driver`].
431    #[doc(hidden)]
432    fn svd_values_with_driver_read(
433        &mut self,
434        input: TensorRead<'_>,
435        _driver: SvdDriver,
436    ) -> tenferro_tensor::Result<Tensor> {
437        self.svd_values_read(input)
438    }
439
440    /// Compute public QR outputs `(Q, R)`.
441    ///
442    /// QR is thin: for an `m x n` input, `Q` has shape `m x min(m, n)` and
443    /// `R` has shape `min(m, n) x n`.
444    ///
445    /// # Errors
446    ///
447    /// Returns `Error::Validation` for an unsupported rank, shape, or dtype,
448    /// and a typed `Error::Extension` or backend source when QR execution
449    /// fails.
450    fn qr(&mut self, input: &Tensor) -> tenferro_tensor::Result<Vec<Tensor>>;
451
452    /// Compute public QR outputs `(Q, R)` with explicit options.
453    ///
454    /// `gauge` controls optional sign or phase post-processing.
455    ///
456    /// # Examples
457    ///
458    /// ```rust
459    /// use tenferro_cpu::{with_cpu_exec_session, CpuBackend};
460    /// use tenferro_linalg::{LinalgBackend, QrGauge, QrOptions};
461    /// use tenferro_tensor::{BackendSessionHost, Tensor};
462    ///
463    /// let input = Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 0.0, 0.0, 2.0])?;
464    /// let mut host = CpuBackend::new();
465    /// let outputs = host.with_backend_session(|session| {
466    ///     with_cpu_exec_session(session, |backend| {
467    ///         backend.qr_with_options(
468    ///             &input,
469    ///             QrOptions::default().gauge(QrGauge::PositiveDiagonal),
470    ///         )
471    ///     })
472    ///     .expect("CpuBackend must expose a CpuExecSession")
473    /// })??;
474    /// assert_eq!(outputs[0].shape(), &[2, 2]);
475    /// # Ok::<(), tenferro_tensor::Error>(())
476    /// ```
477    ///
478    /// # Errors
479    ///
480    /// Returns [`tenferro_tensor::Error::Validation`] containing
481    /// [`tenferro_tensor::ValidationError::RankMismatch`] or
482    /// [`tenferro_tensor::ValidationError::ShapeMismatch`] for an invalid
483    /// matrix input, or [`tenferro_tensor::ValidationError::InvalidArgument`]
484    /// for malformed gauge output metadata, checked size arithmetic, or an
485    /// unavailable compiled provider. A mismatched generated `Q`/`R` dtype is reported as
486    /// [`tenferro_tensor::ValidationError::DTypeMismatch`]. Provider
487    /// unsupported dtype or numerical rejection is
488    /// [`tenferro_tensor::Error::Extension`] with a typed linalg source, while
489    /// provider failures use [`tenferro_tensor::Error::BackendSource`] and a
490    /// backend-resident input uses [`tenferro_tensor::Error::RuntimeState`].
491    fn qr_with_options(
492        &mut self,
493        input: &Tensor,
494        options: QrOptions,
495    ) -> tenferro_tensor::Result<Vec<Tensor>> {
496        let mut outputs = self.qr(input)?;
497        apply_qr_gauge(options.gauge, &mut outputs)?;
498        Ok(outputs)
499    }
500
501    /// Compute public QR outputs `(Q, R)` from a tensor read target.
502    ///
503    /// Backends may canonicalize the input inside the same placement family, but
504    /// must not silently transfer between CPU and GPU memory.
505    ///
506    /// # Examples
507    ///
508    /// ```rust
509    /// use tenferro_linalg::LinalgBackend;
510    /// use tenferro_cpu::{with_cpu_exec_session, CpuBackend};
511    /// use tenferro_tensor::{BackendSessionHost, TensorRead, TensorView, TypedTensor};
512    ///
513    /// let input = TypedTensor::<f64>::from_vec_col_major(
514    ///     vec![2, 2],
515    ///     vec![1.0, 0.0, 0.0, 2.0],
516    /// )?;
517    /// let mut host = CpuBackend::new();
518    /// let outputs = host.with_backend_session(|session| {
519    ///     with_cpu_exec_session(session, |backend| {
520    ///         backend.qr_read(TensorRead::from_view(TensorView::F64(input.as_view())))
521    ///     })
522    ///     .expect("CpuBackend must expose a CpuExecSession")
523    /// })??;
524    /// assert_eq!(outputs[0].shape(), &[2, 2]);
525    /// assert_eq!(outputs[1].shape(), &[2, 2]);
526    /// # Ok::<(), tenferro_tensor::Error>(())
527    /// ```
528    ///
529    /// # Errors
530    ///
531    /// The default implementation returns `Error::Unsupported` because the
532    /// backend does not accept tensor read targets; implementations may return
533    /// validation or typed backend-source errors.
534    fn qr_read(&mut self, _input: TensorRead<'_>) -> tenferro_tensor::Result<Vec<Tensor>> {
535        Err(tenferro_tensor::Error::unsupported(
536            "qr",
537            "backend does not accept tensor reads at this execution boundary",
538        ))
539    }
540
541    /// Compute public QR outputs `(Q, R)` from a tensor read target with options.
542    ///
543    /// Device backends override this hook to keep gauge processing in the input
544    /// placement. The default is appropriate for host backends.
545    ///
546    /// # Examples
547    ///
548    /// ```rust
549    /// use tenferro_cpu::{with_cpu_exec_session, CpuBackend};
550    /// use tenferro_linalg::{LinalgBackend, QrGauge, QrOptions};
551    /// use tenferro_tensor::{BackendSessionHost, Tensor, TensorRead};
552    ///
553    /// let input = Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 0.0, 0.0, 2.0])?;
554    /// let mut host = CpuBackend::new();
555    /// let outputs = host.with_backend_session(|session| {
556    ///     with_cpu_exec_session(session, |backend| {
557    ///         backend.qr_with_options_read(
558    ///             TensorRead::from_tensor(&input),
559    ///             QrOptions::default().gauge(QrGauge::PositiveDiagonal),
560    ///         )
561    ///     })
562    ///     .expect("CpuBackend must expose a CpuExecSession")
563    /// })??;
564    /// assert_eq!(outputs[0].shape(), &[2, 2]);
565    /// # Ok::<(), tenferro_tensor::Error>(())
566    /// ```
567    ///
568    /// # Errors
569    ///
570    /// Returns the validation, unsupported-dtype, numerical, placement, or
571    /// typed backend/provider errors from [`LinalgBackend::qr_read`], plus
572    /// gauge metadata and host-access errors from the default host gauge path.
573    fn qr_with_options_read(
574        &mut self,
575        input: TensorRead<'_>,
576        options: QrOptions,
577    ) -> tenferro_tensor::Result<Vec<Tensor>> {
578        let mut outputs = self.qr_read(input)?;
579        apply_qr_gauge(options.gauge, &mut outputs)?;
580        Ok(outputs)
581    }
582
583    #[doc(hidden)]
584    fn rank_revealing_qr(
585        &mut self,
586        _input: &Tensor,
587        _options: RankRevealingQrOptions,
588    ) -> tenferro_tensor::Result<Vec<Tensor>> {
589        Err(tenferro_tensor::Error::unsupported(
590            "rank_revealing_qr",
591            "backend does not implement rank-revealing QR",
592        ))
593    }
594
595    #[doc(hidden)]
596    fn rank_revealing_qr_read(
597        &mut self,
598        _input: TensorRead<'_>,
599        _options: RankRevealingQrOptions,
600    ) -> tenferro_tensor::Result<Vec<Tensor>> {
601        Err(tenferro_tensor::Error::unsupported(
602            "rank_revealing_qr",
603            "backend does not implement borrowed rank-revealing QR",
604        ))
605    }
606
607    #[doc(hidden)]
608    fn householder_qr(&mut self, _input: &Tensor) -> tenferro_tensor::Result<CompactQrResult> {
609        Err(tenferro_tensor::Error::unsupported(
610            "householder_qr",
611            "backend does not implement compact Householder QR",
612        ))
613    }
614
615    #[doc(hidden)]
616    fn householder_qr_from_factors(
617        &mut self,
618        _q: &Tensor,
619        _r: &Tensor,
620    ) -> tenferro_tensor::Result<CompactQrResult> {
621        Err(tenferro_tensor::Error::unsupported(
622            "householder_qr_from_factors",
623            "backend does not implement compact Householder QR factor import",
624        ))
625    }
626
627    #[doc(hidden)]
628    fn householder_qr_append(
629        &mut self,
630        _packed: &Tensor,
631        _coeff: &Tensor,
632        _block: &Tensor,
633    ) -> tenferro_tensor::Result<CompactQrResult> {
634        Err(tenferro_tensor::Error::unsupported(
635            "householder_qr_append",
636            "backend does not implement compact Householder QR append",
637        ))
638    }
639
640    #[doc(hidden)]
641    fn householder_qr_r(
642        &mut self,
643        _packed: &Tensor,
644        _coeff: &Tensor,
645        _options: QrOptions,
646    ) -> tenferro_tensor::Result<Tensor> {
647        Err(tenferro_tensor::Error::unsupported(
648            "householder_qr_r",
649            "backend does not implement compact Householder QR extraction",
650        ))
651    }
652
653    /// Materialize Q columns `columns` from compact Householder state.
654    ///
655    /// The reachable width is full Q: for an `m x n` input, `columns` may run
656    /// to `m`, and the columns past `k = min(m, n)` span the orthogonal
657    /// complement of the input's column space. `QrOptions::gauge` comes from
658    /// R's diagonal and therefore applies to the first `k` columns only.
659    #[doc(hidden)]
660    fn householder_qr_q_columns(
661        &mut self,
662        _packed: &Tensor,
663        _coeff: &Tensor,
664        _columns: Range<usize>,
665        _options: QrOptions,
666    ) -> tenferro_tensor::Result<Tensor> {
667        Err(tenferro_tensor::Error::unsupported(
668            "householder_qr_q_columns",
669            "backend does not implement compact Householder Q-column materialization",
670        ))
671    }
672
673    /// Compute public Hermitian eigendecomposition outputs `(values, vectors)`.
674    ///
675    /// The returned vector order is `[values, vectors]`, where `values` has
676    /// shape `[n]` and `vectors` has shape `[n, n]`.
677    ///
678    /// # Errors
679    ///
680    /// Returns `Error::Validation` for a non-square or unsupported-dtype input
681    /// and a typed `Error::Extension` or backend source when eigendecomposition
682    /// fails.
683    fn eigh(&mut self, input: &Tensor) -> tenferro_tensor::Result<Vec<Tensor>>;
684
685    /// Compute public Hermitian eigendecomposition outputs with explicit options.
686    ///
687    /// `derivative_eps` is validated for API consistency, but concrete backend
688    /// execution does not perform AD. `gauge` controls optional eigenvector
689    /// post-processing.
690    ///
691    /// # Examples
692    ///
693    /// ```rust
694    /// use tenferro_cpu::{with_cpu_exec_session, CpuBackend};
695    /// use tenferro_linalg::{EighGauge, EighOptions, LinalgBackend};
696    /// use tenferro_tensor::{BackendSessionHost, Tensor};
697    ///
698    /// let input = Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 0.0, 0.0, 2.0])?;
699    /// let mut host = CpuBackend::new();
700    /// let outputs = host.with_backend_session(|session| {
701    ///     with_cpu_exec_session(session, |backend| {
702    ///         backend.eigh_with_options(
703    ///             &input,
704    ///             EighOptions::default()
705    ///                 .gauge(EighGauge::CanonicalPivot)
706    ///                 .derivative_eps(1.0e-10),
707    ///         )
708    ///     })
709    ///     .expect("CpuBackend must expose a CpuExecSession")
710    /// })??;
711    /// assert_eq!(outputs[0].shape(), &[2]);
712    /// # Ok::<(), tenferro_tensor::Error>(())
713    /// ```
714    ///
715    /// # Errors
716    ///
717    /// Returns [`tenferro_tensor::Error::Validation`] containing
718    /// [`tenferro_tensor::ValidationError::InvalidArgument`] when
719    /// `derivative_eps` is non-finite or non-positive, when canonical gauge
720    /// output metadata is malformed, or when checked output-size arithmetic
721    /// overflows. It can return [`tenferro_tensor::Error::Validation`] with
722    /// [`tenferro_tensor::ValidationError::RankMismatch`] or
723    /// [`tenferro_tensor::ValidationError::ShapeMismatch`] for the
724    /// matrix input, or [`tenferro_tensor::ValidationError::DTypeMismatch`]
725    /// for generated outputs. It can also return
726    /// [`tenferro_tensor::Error::Extension`] with typed
727    /// `tenferro_linalg::Error::UnsupportedDType` or `NonConvergence`, and
728    /// [`tenferro_tensor::Error::BackendSource`] or
729    /// [`tenferro_tensor::Error::RuntimeState`] for provider and placement
730    /// failures.
731    fn eigh_with_options(
732        &mut self,
733        input: &Tensor,
734        options: EighOptions,
735    ) -> tenferro_tensor::Result<Vec<Tensor>> {
736        validate_derivative_eps("eigh_with_options", options.derivative_eps)?;
737        let mut outputs = self.eigh(input)?;
738        apply_eigh_gauge(options.gauge, &mut outputs)?;
739        Ok(outputs)
740    }
741
742    /// Compute public eigh outputs `(values, vectors)` from a tensor read
743    /// target with explicit options.
744    ///
745    /// This is the borrowed-input counterpart of
746    /// [`LinalgBackend::eigh_with_options`]. The default implementation runs
747    /// [`LinalgBackend::eigh_read`] and applies the gauge on the host; it
748    /// ignores `driver` because CPU providers have a single eigh kernel. The
749    /// CUDA backend overrides it so the driver reaches cuSOLVER.
750    ///
751    /// # Examples
752    ///
753    /// ```rust
754    /// use tenferro_cpu::{with_cpu_exec_session, CpuBackend};
755    /// use tenferro_linalg::{EighDriver, EighOptions, LinalgBackend};
756    /// use tenferro_tensor::{BackendSessionHost, TensorRead, TensorView, TypedTensor};
757    ///
758    /// let input = TypedTensor::<f64>::from_vec_col_major(
759    ///     vec![2, 2],
760    ///     vec![2.0, 0.0, 0.0, 3.0],
761    /// )?;
762    /// let mut host = CpuBackend::new();
763    /// let outputs = host.with_backend_session(|session| {
764    ///     with_cpu_exec_session(session, |backend| {
765    ///         backend.eigh_with_options_read(
766    ///             TensorRead::from_view(TensorView::F64(input.as_view())),
767    ///             EighOptions::default().driver(EighDriver::Syevj),
768    ///         )
769    ///     })
770    ///     .expect("CpuBackend must expose a CpuExecSession")
771    /// })??;
772    /// assert_eq!(outputs[0].shape(), &[2]);
773    /// # Ok::<(), tenferro_tensor::Error>(())
774    /// ```
775    ///
776    /// # Errors
777    ///
778    /// Returns [`tenferro_tensor::Error::Validation`] containing
779    /// [`tenferro_tensor::ValidationError::InvalidArgument`] when
780    /// `derivative_eps` is non-finite or non-positive, plus the validation,
781    /// unsupported-dtype, numerical, placement, or typed backend/provider
782    /// errors from [`LinalgBackend::eigh_read`] and the gauge metadata and
783    /// host-access errors from the default host gauge path.
784    fn eigh_with_options_read(
785        &mut self,
786        input: TensorRead<'_>,
787        options: EighOptions,
788    ) -> tenferro_tensor::Result<Vec<Tensor>> {
789        validate_derivative_eps("eigh_with_options_read", options.derivative_eps)?;
790        let mut outputs = self.eigh_read(input)?;
791        apply_eigh_gauge(options.gauge, &mut outputs)?;
792        Ok(outputs)
793    }
794
795    /// Compute public Hermitian eigendecomposition outputs from a tensor read target.
796    ///
797    /// Backends may canonicalize the input inside the same placement family, but
798    /// must not silently transfer between CPU and GPU memory.
799    ///
800    /// # Examples
801    ///
802    /// ```rust
803    /// use tenferro_linalg::LinalgBackend;
804    /// use tenferro_cpu::{with_cpu_exec_session, CpuBackend};
805    /// use tenferro_tensor::{BackendSessionHost, TensorRead, TensorView, TypedTensor};
806    ///
807    /// let input = TypedTensor::<f64>::from_vec_col_major(
808    ///     vec![2, 2],
809    ///     vec![1.0, 0.0, 0.0, 2.0],
810    /// )?;
811    /// let mut host = CpuBackend::new();
812    /// let outputs = host.with_backend_session(|session| {
813    ///     with_cpu_exec_session(session, |backend| {
814    ///         backend.eigh_read(TensorRead::from_view(TensorView::F64(input.as_view())))
815    ///     })
816    ///     .expect("CpuBackend must expose a CpuExecSession")
817    /// })??;
818    /// assert_eq!(outputs[0].shape(), &[2]);
819    /// assert_eq!(outputs[1].shape(), &[2, 2]);
820    /// # Ok::<(), tenferro_tensor::Error>(())
821    /// ```
822    ///
823    /// # Errors
824    ///
825    /// The default implementation returns `Error::Unsupported` because the
826    /// backend does not accept tensor read targets; implementations may return
827    /// validation or typed backend-source errors.
828    fn eigh_read(&mut self, _input: TensorRead<'_>) -> tenferro_tensor::Result<Vec<Tensor>> {
829        Err(tenferro_tensor::Error::unsupported(
830            "eigh",
831            "backend does not accept tensor reads at this execution boundary",
832        ))
833    }
834
835    /// Compute Cholesky factorization from a tensor read target.
836    ///
837    /// Backends may canonicalize the input inside the same placement family, but
838    /// must not silently transfer between CPU and GPU memory.
839    ///
840    /// # Examples
841    ///
842    /// ```rust
843    /// use tenferro_linalg::LinalgBackend;
844    /// use tenferro_cpu::{with_cpu_exec_session, CpuBackend};
845    /// use tenferro_tensor::{BackendSessionHost, TensorRead, TensorView, TypedTensor};
846    ///
847    /// let input = TypedTensor::<f64>::from_vec_col_major(
848    ///     vec![2, 2],
849    ///     vec![4.0, 2.0, 2.0, 3.0],
850    /// )?;
851    /// let mut host = CpuBackend::new();
852    /// let output = host.with_backend_session(|session| {
853    ///     with_cpu_exec_session(session, |backend| {
854    ///         backend.cholesky_read(TensorRead::from_view(TensorView::F64(input.as_view())))
855    ///     })
856    ///     .expect("CpuBackend must expose a CpuExecSession")
857    /// })??;
858    /// assert_eq!(output.shape(), &[2, 2]);
859    /// # Ok::<(), tenferro_tensor::Error>(())
860    /// ```
861    ///
862    /// # Errors
863    ///
864    /// The default implementation returns `Error::Unsupported` because the
865    /// backend does not accept tensor read targets; implementations may return
866    /// validation or typed backend-source errors.
867    fn cholesky_read(&mut self, _input: TensorRead<'_>) -> tenferro_tensor::Result<Tensor> {
868        Err(tenferro_tensor::Error::unsupported(
869            "cholesky",
870            "backend does not accept tensor reads at this execution boundary",
871        ))
872    }
873
874    /// Compute public LU outputs from a tensor read target.
875    ///
876    /// Backends may canonicalize the input inside the same placement family, but
877    /// must not silently transfer between CPU and GPU memory.
878    ///
879    /// # Examples
880    ///
881    /// ```rust
882    /// use tenferro_linalg::LinalgBackend;
883    /// use tenferro_cpu::{with_cpu_exec_session, CpuBackend};
884    /// use tenferro_tensor::{BackendSessionHost, TensorRead, TensorView, TypedTensor};
885    ///
886    /// let input = TypedTensor::<f64>::from_vec_col_major(
887    ///     vec![2, 2],
888    ///     vec![1.0, 3.0, 2.0, 4.0],
889    /// )?;
890    /// let mut host = CpuBackend::new();
891    /// let outputs = host.with_backend_session(|session| {
892    ///     with_cpu_exec_session(session, |backend| {
893    ///         backend.lu_read(TensorRead::from_view(TensorView::F64(input.as_view())))
894    ///     })
895    ///     .expect("CpuBackend must expose a CpuExecSession")
896    /// })??;
897    /// assert_eq!(outputs.len(), 4);
898    /// # Ok::<(), tenferro_tensor::Error>(())
899    /// ```
900    ///
901    /// # Errors
902    ///
903    /// The default implementation returns `Error::Unsupported` because the
904    /// backend does not accept tensor read targets; implementations may return
905    /// validation or typed backend-source errors.
906    fn lu_read(&mut self, _input: TensorRead<'_>) -> tenferro_tensor::Result<Vec<Tensor>> {
907        Err(tenferro_tensor::Error::unsupported(
908            "lu",
909            "backend does not accept tensor reads at this execution boundary",
910        ))
911    }
912
913    /// Compute public full-pivoting LU outputs from a tensor read target.
914    ///
915    /// Backends may canonicalize the input inside the same placement family, but
916    /// must not silently transfer between CPU and GPU memory.
917    ///
918    /// # Examples
919    ///
920    /// ```rust
921    /// use tenferro_linalg::LinalgBackend;
922    /// use tenferro_cpu::{with_cpu_exec_session, CpuBackend};
923    /// use tenferro_tensor::{BackendSessionHost, TensorRead, TensorView, TypedTensor};
924    ///
925    /// let input = TypedTensor::<f64>::from_vec_col_major(
926    ///     vec![2, 2],
927    ///     vec![1.0, 3.0, 2.0, 4.0],
928    /// )?;
929    /// let mut host = CpuBackend::new();
930    /// let outputs = host.with_backend_session(|session| {
931    ///     with_cpu_exec_session(session, |backend| {
932    ///         backend.full_piv_lu_read(TensorRead::from_view(TensorView::F64(input.as_view())))
933    ///     })
934    ///     .expect("CpuBackend must expose a CpuExecSession")
935    /// })??;
936    /// assert_eq!(outputs.len(), 5);
937    /// # Ok::<(), tenferro_tensor::Error>(())
938    /// ```
939    ///
940    /// # Errors
941    ///
942    /// The default implementation returns `Error::Unsupported` because the
943    /// backend does not accept tensor read targets; implementations may return
944    /// validation or typed backend-source errors.
945    fn full_piv_lu_read(&mut self, _input: TensorRead<'_>) -> tenferro_tensor::Result<Vec<Tensor>> {
946        Err(tenferro_tensor::Error::unsupported(
947            "full_piv_lu",
948            "backend does not accept tensor reads at this execution boundary",
949        ))
950    }
951
952    /// Compute general eigendecomposition outputs from a tensor read target.
953    ///
954    /// Backends may canonicalize the input inside the same placement family, but
955    /// must not silently transfer between CPU and GPU memory.
956    ///
957    /// # Examples
958    ///
959    /// ```rust
960    /// use tenferro_linalg::LinalgBackend;
961    /// use tenferro_cpu::{with_cpu_exec_session, CpuBackend};
962    /// use tenferro_tensor::{BackendSessionHost, TensorRead, TensorView, TypedTensor};
963    ///
964    /// let input = TypedTensor::<f64>::from_vec_col_major(
965    ///     vec![2, 2],
966    ///     vec![2.0, 0.0, 0.0, 3.0],
967    /// )?;
968    /// let mut host = CpuBackend::new();
969    /// let outputs = host.with_backend_session(|session| {
970    ///     with_cpu_exec_session(session, |backend| {
971    ///         backend.eig_read(TensorRead::from_view(TensorView::F64(input.as_view())))
972    ///     })
973    ///     .expect("CpuBackend must expose a CpuExecSession")
974    /// })??;
975    /// assert_eq!(outputs.len(), 2);
976    /// # Ok::<(), tenferro_tensor::Error>(())
977    /// ```
978    ///
979    /// # Errors
980    ///
981    /// The default implementation returns `Error::Unsupported` because the
982    /// backend does not accept tensor read targets; implementations may return
983    /// validation or typed backend-source errors.
984    fn eig_read(&mut self, _input: TensorRead<'_>) -> tenferro_tensor::Result<Vec<Tensor>> {
985        Err(tenferro_tensor::Error::unsupported(
986            "eig",
987            "backend does not accept tensor reads at this execution boundary",
988        ))
989    }
990
991    #[doc(hidden)]
992    fn eigh_values(&mut self, _input: &Tensor) -> tenferro_tensor::Result<Tensor> {
993        Err(tenferro_tensor::Error::unsupported(
994            "eigh_values",
995            format!(
996                "backend {} does not implement internal Hermitian eigenvalues-only decomposition",
997                std::any::type_name::<Self>()
998            ),
999        ))
1000    }
1001
1002    /// Eigenvalues only with an explicit CUDA driver. The default
1003    /// implementation ignores the driver, matching
1004    /// [`LinalgBackend::eigh_with_options`].
1005    #[doc(hidden)]
1006    fn eigh_values_with_driver(
1007        &mut self,
1008        input: &Tensor,
1009        _driver: EighDriver,
1010    ) -> tenferro_tensor::Result<Tensor> {
1011        self.eigh_values(input)
1012    }
1013
1014    #[doc(hidden)]
1015    fn eigh_values_read(&mut self, _input: TensorRead<'_>) -> tenferro_tensor::Result<Tensor> {
1016        Err(tenferro_tensor::Error::unsupported(
1017            "eigh_values",
1018            "backend does not implement borrowed Hermitian eigenvalues-only decomposition",
1019        ))
1020    }
1021
1022    /// Borrowed-input counterpart of
1023    /// [`LinalgBackend::eigh_values_with_driver`].
1024    #[doc(hidden)]
1025    fn eigh_values_with_driver_read(
1026        &mut self,
1027        input: TensorRead<'_>,
1028        _driver: EighDriver,
1029    ) -> tenferro_tensor::Result<Tensor> {
1030        self.eigh_values_read(input)
1031    }
1032
1033    /// Compute public general eigendecomposition outputs `(values, vectors)`.
1034    ///
1035    /// # Errors
1036    ///
1037    /// Returns `Error::Validation` for a non-square, rank, or dtype mismatch,
1038    /// and a typed `Error::Extension` or backend source when the eigensolver
1039    /// fails.
1040    fn eig(&mut self, input: &Tensor) -> tenferro_tensor::Result<Vec<Tensor>>;
1041
1042    #[doc(hidden)]
1043    fn eig_values(&mut self, _input: &Tensor) -> tenferro_tensor::Result<Tensor> {
1044        Err(tenferro_tensor::Error::unsupported(
1045            "eig_values",
1046            format!(
1047                "backend {} does not implement internal general eigenvalues-only decomposition",
1048                std::any::type_name::<Self>()
1049            ),
1050        ))
1051    }
1052
1053    #[doc(hidden)]
1054    fn eig_values_read(&mut self, _input: TensorRead<'_>) -> tenferro_tensor::Result<Tensor> {
1055        Err(tenferro_tensor::Error::unsupported(
1056            "eig_values",
1057            "backend does not implement borrowed general eigenvalues-only decomposition",
1058        ))
1059    }
1060
1061    /// Solve a dense linear system.
1062    ///
1063    /// # Errors
1064    ///
1065    /// Returns `Error::Validation` for incompatible matrix/rhs shapes, rank,
1066    /// or dtype; `Error::Extension` with `ErrorKind::NumericalFailure` for a
1067    /// singular system; or a typed backend source for provider failure.
1068    fn solve(&mut self, a: &Tensor, b: &Tensor) -> tenferro_tensor::Result<Tensor>;
1069
1070    /// Solve a linear system from tensor read targets.
1071    ///
1072    /// Backends may canonicalize the inputs inside the same placement family,
1073    /// but must not silently transfer between CPU and GPU memory.
1074    ///
1075    /// # Examples
1076    ///
1077    /// ```rust
1078    /// use tenferro_cpu::{with_cpu_exec_session, CpuBackend};
1079    /// use tenferro_linalg::LinalgBackend;
1080    /// use tenferro_tensor::{BackendSessionHost, Tensor, TensorRead};
1081    ///
1082    /// let a = Tensor::from_vec_col_major(vec![2, 2], vec![2.0_f64, 0.0, 0.0, 3.0])?;
1083    /// let b = Tensor::from_vec_col_major(vec![2, 1], vec![4.0_f64, 9.0])?;
1084    /// let mut host = CpuBackend::new();
1085    /// let x = host.with_backend_session(|session| {
1086    ///     with_cpu_exec_session(session, |backend| {
1087    ///         backend.solve_read(
1088    ///             TensorRead::from_tensor(&a),
1089    ///             TensorRead::from_tensor(&b),
1090    ///         )
1091    ///     })
1092    ///     .expect("CpuBackend must expose a CpuExecSession")
1093    /// })??;
1094    /// let x = x.into_typed::<f64>().expect("F64 inputs return F64 output");
1095    /// assert_eq!(x.host_data()?, &[2.0, 3.0]);
1096    /// # Ok::<(), tenferro_tensor::Error>(())
1097    /// ```
1098    ///
1099    /// # Errors
1100    ///
1101    /// The default implementation returns `Error::Unsupported` because the
1102    /// backend does not accept tensor read targets. Implementations may return
1103    /// `Error::Validation` for incompatible shapes or dtypes,
1104    /// `Error::RuntimeState` for invalid placement, `Error::Extension` for a
1105    /// singular system, or a typed backend-source error.
1106    fn solve_read(
1107        &mut self,
1108        _a: TensorRead<'_>,
1109        _b: TensorRead<'_>,
1110    ) -> tenferro_tensor::Result<Tensor> {
1111        Err(tenferro_tensor::Error::unsupported(
1112            "solve",
1113            "backend does not accept tensor reads at this execution boundary",
1114        ))
1115    }
1116
1117    /// Solve into a caller-owned destination.
1118    ///
1119    /// The default preserves the ordinary read path and copies its result into
1120    /// `out`. Backends with a native destination path may override this method,
1121    /// but must validate the destination before the first write and preserve
1122    /// the same shape, dtype, placement, aliasing, and error contracts.
1123    ///
1124    /// # Errors
1125    ///
1126    /// Returns `tenferro_tensor_core::ShapeMismatch` or
1127    /// `tenferro_tensor_core::ValidationError::DTypeMismatch` for incompatible
1128    /// destination metadata, `tenferro_tensor_core::ValidationError::InvalidArgument`
1129    /// for aliasing or placement violations, `Error::Unsupported` when the
1130    /// provider is unavailable, and `Error::Singular` for a singular system.
1131    ///
1132    /// # Examples
1133    ///
1134    /// ```rust
1135    /// use tenferro_cpu::{with_cpu_exec_session, CpuBackend};
1136    /// use tenferro_linalg::LinalgBackend;
1137    /// use tenferro_tensor::{BackendSessionHost, Tensor, TensorRead, TensorWrite};
1138    ///
1139    /// let a = Tensor::from_vec_col_major(vec![2, 2], vec![2.0_f64, 0.0, 0.0, 4.0])?;
1140    /// let b = Tensor::from_vec_col_major(vec![2, 1], vec![4.0_f64, 8.0])?;
1141    /// let mut out = Tensor::from_vec_col_major(vec![2, 1], vec![0.0_f64; 2])?;
1142    /// let mut host = CpuBackend::new();
1143    /// host.with_backend_session(|session| {
1144    ///     with_cpu_exec_session(session, |backend| {
1145    ///         backend.solve_read_into(
1146    ///             TensorRead::from_tensor(&a),
1147    ///             TensorRead::from_tensor(&b),
1148    ///             TensorWrite::from_tensor(&mut out),
1149    ///         )
1150    ///     })
1151    ///     .expect("CpuBackend must expose a CpuExecSession")
1152    /// })??;
1153    /// assert_eq!(out.as_slice::<f64>()?, &[2.0, 2.0]);
1154    /// # Ok::<(), tenferro_tensor::Error>(())
1155    /// ```
1156    fn solve_read_into(
1157        &mut self,
1158        a: TensorRead<'_>,
1159        b: TensorRead<'_>,
1160        out: TensorWrite<'_>,
1161    ) -> tenferro_tensor::Result<()> {
1162        solve_read_into_default(self, a, b, out)
1163    }
1164
1165    #[doc(hidden)]
1166    fn lu_solve_prepared(
1167        &mut self,
1168        _a: &Tensor,
1169        _packed_lu: &Tensor,
1170        _pivots: &Tensor,
1171        _b: &Tensor,
1172        _transpose_a: bool,
1173        _conjugate_a: bool,
1174    ) -> tenferro_tensor::Result<Tensor> {
1175        Err(tenferro_tensor::Error::unsupported(
1176            "lu_solve_prepared",
1177            format!(
1178                "backend {} does not implement internal prepared LU solve",
1179                std::any::type_name::<Self>()
1180            ),
1181        ))
1182    }
1183
1184    /// Factor `a` and solve `a x = b`, returning `(x, packed_lu, pivots)`.
1185    ///
1186    /// This is the fused primal of `lu_factor` followed by
1187    /// `lu_solve_prepared`. The default composes those two hooks; a backend
1188    /// overrides it to run both steps in one batched kernel without the
1189    /// intermediate parity output.
1190    ///
1191    /// # Errors
1192    ///
1193    /// Returns the errors of `lu_factor` and `lu_solve_prepared`, including
1194    /// `Error::Extension` for an exactly singular `a` with a nonempty `b`.
1195    #[doc(hidden)]
1196    fn lu_factor_solve(&mut self, a: &Tensor, b: &Tensor) -> tenferro_tensor::Result<Vec<Tensor>> {
1197        let mut factors = self.lu_factor(a)?.into_iter();
1198        let (Some(packed_lu), Some(pivots)) = (factors.next(), factors.next()) else {
1199            return Err(tenferro_tensor::Error::Internal(
1200                "lu_factor_solve: lu_factor returned fewer than two outputs".into(),
1201            ));
1202        };
1203        let x = self.lu_solve_prepared(a, &packed_lu, &pivots, b, false, false)?;
1204        Ok(vec![x, packed_lu, pivots])
1205    }
1206}
1207
1208pub(crate) fn solve_read_into_default<B: LinalgBackend + ?Sized>(
1209    backend: &mut B,
1210    a: TensorRead<'_>,
1211    b: TensorRead<'_>,
1212    out: TensorWrite<'_>,
1213) -> tenferro_tensor::Result<()> {
1214    validate_solve_read_into(&a, &b, &out)?;
1215    let result = backend.solve_read(a, b)?;
1216    backend.copy_read_into(TensorRead::from_tensor(&result), out)
1217}
1218
1219pub(crate) fn validate_solve_read_into(
1220    _a: &TensorRead<'_>,
1221    b: &TensorRead<'_>,
1222    out: &TensorWrite<'_>,
1223) -> tenferro_tensor::Result<()> {
1224    if b.shape() != out.shape() {
1225        return Err(tenferro_tensor::Error::shape_mismatch(
1226            "solve_read_into",
1227            b.shape().to_vec(),
1228            out.shape().to_vec(),
1229        ));
1230    }
1231    if b.dtype() != out.dtype() {
1232        return Err(tenferro_tensor::Error::dtype_mismatch(
1233            "solve_read_into",
1234            b.dtype(),
1235            out.dtype(),
1236        ));
1237    }
1238    if b.placement() != out.as_read().placement() {
1239        return Err(tenferro_tensor::Error::invalid_argument(
1240            "solve_read_into",
1241            "out",
1242            format!(
1243                "destination placement {:?} does not match rhs placement {:?}",
1244                out.as_read().placement(),
1245                b.placement()
1246            ),
1247        ));
1248    }
1249    let inputs = [_a.clone(), b.clone()];
1250    tenferro_tensor::backend::validate_read_into_destination("solve_read_into", &inputs, out)
1251}